Wrapping useState hook with custom setter and clean-up functionality - reactjs

I'd like to create a custom React hook that
wraps a state (created with useState),
runs a clean-up function up when the component unmounts,
executes a custom setter logic that derives a value,
and exports the custom setter and the internal state value.
export function useCustomState() {
const [value, setValue] = useState();
useEffect(() => {
return () => {
// do cleanup
};
}, [value]);
function customSetValue(newValue) {
if (value) {
// do cleanup for the previous value
}
const derivedValue = derive(newValue);
setValue(derivedValue);
}
return [value, customSetValue];
}
I can achieve this with the code above. My problem arises when I use the returned values in an useEffect hook as a dependency, since the returned custom setter is always a new function reference.
const Component = () => {
const [value, setValue] = useCustomState();
useEffect(
() => {
setValue(simpleValue);
},
[setValue],
);
return <p>{ value }</p>;
};
When I don't include the setter as a dependency of the useEffect, the re-rendering stops, since the dependency does not change after a render. I can omit that reference and disable eslint for the line. This is one solution.
I'd like to know whether it is possible to create a custom referentially stable setter function?
I also have tried using useMemo that stores the custom setter function, but the dependencies for the memo still include the internal value and setValue references, since I want to do clean-up and set the new derived value. If the derived value is not the same for the same input, the dependency cycle will result in infinite re-rendering.

You can pass a function to setValue to remove the dependence on value; instead, the current value is passed as an argument. As a simpler alternative to useMemo, useCallback will give you a consistent function:
export function useCustomState() {
const [value, setValue] = useState();
useEffect(() => {
return () => {
// do cleanup
};
}, [value]);
const customSetValue = useCallback((newValue) => {
setValue((oldValue) => {
if (oldValue) {
// do cleanup for the previous value
}
const derivedValue = derive(newValue);
return derivedValue;
});
}, []);
return [value, customSetValue];
}

Related

React - set state doesn't change in callback function

I'm not able to read current state inside refreshWarehouseCallback function. Why?
My component:
export function Schedules({ tsmService, push, pubsub }: Props) {
const [myState, setMyState] = useState<any>(initialState);
useEffect(() => {
service
.getWarehouses()
.then((warehouses) =>
getCurrentWarehouseData(warehouses) // inside of this function I can without problems set myState
)
.catch(() => catchError());
const pushToken = push.subscribe('public/ttt/#');
const pubSubToken = pubsub.subscribe(
'push:ttt.*',
refreshWarehouseCallback // HERE IS PROBLEM, when I try to read current state from this function I get old data, state changed in other functions cannot be read in thi function
);
return () => {
pubsub.unsubscribe(pubSubToken);
push.unsubscribe(pushToken);
};
}, []);
...
function refreshWarehouseCallback(eventName: string, content: any) {
const {warehouseId} = myState; // undefined!!!
case pushEvents.ramp.updated: {
}
}
return (
<Views
warehouses={myState.warehouses}
allRamps={myState.allRamps}
currentWarehouse={myState.currentWarehouse}
pending={myState.pending}
error={myState.error}
/>
I have to use useRef to store current state additionally to be able to rerender the whole component.
My question is - is there any other solution without useRef? Where is the problem? Calback function doesn't work with useState hook?
Your pub/sub pattern does not inherit React's states. Whenever subscribe is triggered, and your callback function is initialized, that callback will not get any new values from myState.
To be able to use React's states, you can wrap refreshWarehouseCallback into another function like below
//`my state` is passed into the first function (the function wrapper)
//the inner function is your original function
const refreshWarehouseCallback =
(myState) => (eventName: string, content: any) => {
const { warehouseId } = myState;
//your other logic
};
And then you can add another useEffect to update subscribe after state changes (in this case, myState updates)
//a new state to store the updated pub/sub after every clean-up
const [pubSubToken, setPubSubToken] = useState();
useEffect(() => {
//clean up when your state updates
if (pubSubToken) {
pubsub.unsubscribe(pubSubToken);
}
const updatedPubSubToken = pubsub.subscribe(
"push:ttt.*",
refreshWarehouseCallback(myState) //execute the function wrapper to pass `myState` down to your original callback function
);
//update new pub/sub token
setPubSubToken(updatedPubSubToken);
return () => {
pubsub.unsubscribe(updatedPubSubToken);
};
//add `myState` as a dependency
}, [myState]);
//you can combine this with your previous useEffect
useEffect(() => {
const pushToken = push.subscribe("public/ttt/#");
return () => {
pubsub.unsubscribe(pushToken);
};
}, []);

Stale custom hook state property on callback

I have a custom hook in my React app that exposes a function (hookFn) to calculate a value. Once the value has been updated (state change, triggering useEffect), the hook alerts the app via a callback function. Here's the issue: in my callback function, I want to be able to access the value via hook.value, but it seems to be stale! Even though I know the value state has been updated!
Codesandbox: https://codesandbox.io/s/stoic-payne-bwp6j5?file=/src/App.js:0-910
import { useEffect, useRef, useState } from "react";
export default function App() {
const hook = useCustomHook();
useEffect(() => {
hook.hookFn(hookCallback);
}, []);
function hookCallback(value) {
console.log({givenValue: value, hookValue: hook.value});
}
return "See console for output";
}
function useCustomHook() {
const callbackRef = useRef(null);
const [value, setValue] = useState("initial value");
useEffect(() => {
if (callbackRef.current) {
callbackRef.current(value);
}
}, [value]);
function hookFn(callbackFn) {
callbackRef.current = callbackFn;
setValue("value set in hookFn");
}
return { hookFn, value };
}
FYI: in my actual app, the hook is for searching, which may call the callback function multiple times as more search results become available.
Is there any way to ensure hook.value will be valid? Or is it bad practice for a hook to expose a state variable in general?
It turns out hook.value is stale because hook is stale when I access it from hookCallback. Each time there is a state change within my custom hook, useCustomHook will generate a new object.
The complex solution, then, is to to create a ref for hook and keep it up to date in useEffect. But then I have to make sure I wait for that useEffect to run before accessing hookRef.current.value... Here's my attempt to make this work: https://codesandbox.io/s/dazzling-shirley-0r7k47?file=/src/App.js
However, a better solution: don't mix React states and manual callbacks. Instead, just watch for state changes in a useEffect, like so:
import { useEffect, useState } from "react";
export default function App() {
const hook = useCustomHook();
useEffect(() => {
hook.hookFn();
}, []);
useEffect(() => {
if (hook.value) console.log({ hookValue: hook.value });
}, [hook.value]);
return "See console for output";
}
function useCustomHook() {
const [value, setValue] = useState("initial value");
function hookFn(callbackFn) {
setValue("value set in hookFn");
}
return { hookFn, value };
}
Notice the code is simplified, and there's no need for concern about states being out-of-sync.
I think you have pretty much answered you own question. Alternatively, you could pass your callback function as input to your custom hook.
import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const hook = useCustomHook(hookCallback);
useEffect(() => {
hook.setNewValue();
},[])
function hookCallback(value) {
console.log({
givenValue: value,
hookValue: hook.value, // Why is this stale??
areIdentical: value === hook.value // Should be true!!!
});
}
return <h1>See console for output</h1>;
}
function useCustomHook(callback) {
const [value, setValue] = useState("initial value");
useEffect(() => {
callback(value);
}, [value]);
function setNewValue(callbackFn) {
setValue("value set in hookFn");
setTimeout(() => {
setValue("value set in setTimeout");
}, 100);
}
return { setNewValue, value };
}

Best practice for marking hooks as not to be reused in multiple places

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 };
};

Custom react hook can not be called in useEffect with empty dependencies

Im new to react hook, Im doing a project with new feature "Hooks" of react.
I've faced a problem and I need an explain for it.
As document, to implement "componentDidMount", just pass empty array in dependencies argument.
useEffect(() => {
// some code here
}, []);
And I can call dispatch function to updateState inside this useEffect.
const [flag, setFlag] = useState(false);
useEffect(() => {
setFlag(true);
}, []);
Above code works perfectly without warning or any errors.
Now I have my custom hook, but I can not call my dispatch inside the effect.
const [customFlag, setCustomFlag] = useCustomHook();
useEffect(() => {
setCustomFlag(true);
}, []);
This is my custom hook.
function useCustomHook() {
const [success, setSuccess] = useState(false):
const component = <div>{ success ? "Success" : "Fail" }</div>;
const dispatch = useCallback(success => {
setSuccess(success);
}, []);
return [component, dispatch];
}
With above code, it requires me to put setCustomFlag inside the dependencies array.
I do not understand why. What is different between them?
Thanks for sharing.
Probably, your custom hook returns different instance of setCustomFlag on each call. It means, that useEffect() will always use first value (returned on first render). Try to memoize it by calling useCallback()/useMemo() hooks:
function useCustomHook() {
...
const setCustomFlag = useCallback(/* setCustomFlag body here */, []);
}
It would be nice to have your custom hook source to say more.
The reason is setFlag from useState is a known dependency, its value won't change between render, hence you don't have to declare it as a dependency
React eslint-plugin-react-hooks can't be sure about your custom hook, that's why you need to put that into the dependency list
This is taken from eslint-plugin-react-hooks
// Next we'll define a few helpers that helps us
// tell if some values don't have to be declared as deps.
// Some are known to be static based on Hook calls.
// const [state, setState] = useState() / React.useState()
// ^^^ true for this reference
// const [state, dispatch] = useReducer() / React.useReducer()
// ^^^ true for this reference
// const ref = useRef()
// ^^^ true for this reference
// False for everything else.
function isStaticKnownHookValue(resolved) {
}

useEffect lazy created cleanup function

I'm trying to create hook that is is using an effect in which side effect function returns the cleanup callback. However I want to call it only when component is unmounted, not on the rerender.
Normal approach when you call useEffect with empty deps array won't work here as the cleanup function is created only once, on the first call of the hook. But my clean up is created later, so there is no way to change it.
function useListener(data) {
const [response, updateResponse] = useState(null);
useEffect(
() => {
if (data) {
const removeListener = callRequest(data, resp => {
updateResponse(resp);
});
return removeListener;
}
},
[data]
);
return response;
}
This comes down to a following problem: In normal class component, the willComponentUnmount could make a decision based on a current component state but in case of useEffect, state is passed via closure to the cleanup and there is no way to pass the information later if the state has changed
You can use useRef to save and update your callback function
The useRef() Hook isn’t just for DOM refs. The “ref” object is a generic container whose current property is mutable and can hold any value, similar to an instance property on a class. more
function useListener(data) {
const [response, updateResponse] = useState(null);
const cleanUpCallbackRef = useRef(() => {});
useEffect(
() => {
if (data) {
cleanUpCallbackRef.current = callRequest(data, resp => {
updateResponse(resp);
});
}
},
[data]
);
useEffect(() => {
return () => {
cleanUpCallbackRef.current();
}
}, []);
return response;
}
I create a simple example here

Resources