I successfully applied local storage in my component and would like to create my first hook, which will take care of the local storage, so I can re-use it in my other components.
/* EXAMPLE */
//useState:
const [state,setState] = useState("");
setValue("Hello"); // WORKS!
//useLocalHook:
const [hook_value,setHookValue] = useLocalStorage("");
setHookValue("New Value"); // NOT WORKING
I read guides about custom hooks, but they are not using the setFunction for their custom hooks, is this impossible to do?
Parent:
// Local storage
const [data,setData] = useLocalStorage("mydata","");
useEffect(()=>{
setData(new_value);
},[data]);
Hook:
const useLocalStorage = (storage_key,initial_value) => {
const [value,setValue] = useState(getLocalStorage(storage_key,initial_value));
/* LOCAL STORAGE */
function saveLocalStorage(key,value){
if (typeof window !== "undefined") {
// Set New Default Value
const saved_value = JSON.stringify(value);
console.log(`Key:${key} stored:${value}`);
localStorage.setItem(key,saved_value);
}
}
function getLocalStorage(key,initial_value)
{
if (typeof window !== "undefined") {
const value = JSON.parse(localStorage.getItem(key));
console.log(`Key:${key} received:${value}`);
if(value)
{
return value;
}
}
// Not found, return initial value
return initial_value;
}
function clearLocalStorage()
{
localStorage.clear();
}
function setValue(new_val)
{
value = new_val;
}
// Save settings in local storage
useEffect(()=>{
saveLocalStorage(storage_key,value);
},[value]);
// Return value
return [value];
}
What am I not understanding /& doing wrong if it's possible to change the value with custom hooks?
You need to return setValuefn in your custom hook, so you can use used it inside useEffect
Hook:
const useLocalStorage = (storageKey,initialValue) => {
const [value,setValue] = useState(getLocalStorage(storageKey,initialValue));
return [value, setValue]
And now in the parent you can use setData
Parent
const [data,setData] = useLocalStorage("mydata","");
useEffect(()=>{
setData(new_value);
},[data]);
Related
So, I want to do the following,
const [value, setValue] = useState({})
const updateName = (name)
setValue(previousState => {
if (/*check some conditions*/) {
// dont update the state
} else {
return { /* some new state */ }
}
});
Is there anyway how can i achieve it?
You are almost there.
const [value, setValue] = useState({})
setValue((prevState) => {
if (condition to not update) {
return prevState;
} else {
return newState;
}
});
You don't necessarily need to do it from the setValue call. You can also do it like this:
const updateSomething = () => {
if (shouldUpdate)
setValue(newState);
}
You can try with something simple like:
setValue(!value)
That will just change a boolean state, passing from false value to true value and vice versa.
I hope it can help.
I am using the geolocation API inside my functional component, and my success and error callbacks for geolocation.watchPosition(success, error) have conditional code that is dependent on some state value that is going to change.
I do not want to initiate geolocation.watchPosition on mount inside useEffect.
I tried wrapping the callbacks with useCallback and passing the state value to the dependency array, but the callback still closes over the stale state value even though it is updated. Is there any workaround to this?
Simplified example:
function Map() {
const watchRef = useRef();
const [isBool, setIsBool] = useState(false);
function init() {
watchRef.current = navigator.geolocation.watchPosition(success, error);
}
const success = useCallback((pos) => {
if(isBool) {
// do something...
return;
}
// do something else...
}, [isBool])
function updateStateHandler() {
setIsBool(true);
}
return // jsx...
}
Figured out a solution using refs to break out of the closure.
Solution:
function Map() {
const watchRef = useRef();
const isBoolRef = useRef(); // create a ref
const [isBool, setIsBool] = useState(false);
function init() {
watchRef.current = navigator.geolocation.watchPosition(success, error);
}
// no need for useCallback
const success = (pos) => {
if(isBoolRef.current) {
// do something...
return;
}
// do something else...
}
function updateStateHandler() {
setIsBool(true);
}
useEffect(() => {
isBoolRef.current = isBool; // this will return the updated value inside the callback
}, [isBool])
return // jsx...
}
I have a custom React hook something like this:
export default function useLocations(locationsToMatch) {
const state = useAnotherHookToGetStateFromStore();
const { allStores } = state.locations;
const allLocations = {};
allStores.forEach((store) => {
const { locationId, locationType } = store;
const isLocationPresent = locationsToMatch.indexOf(locationId) !== -1;
if (isLocationPresent && locationType === 'someValue') {
allLocations[locationId] = true;
} else {
allLocations[locationId] = false;
}
});
return allLocations;
}
When I use above hook inside my React component like this:
const locations = useLocations([908, 203, 678]) // pass location ids
I get a max call depth error due to infinite rendering. This is because I have some code inside my component which uses useEffect hook like this:
useEffect(() => { // some code to re-render component on change of locations
}, [locations])
So I tried to wrap my return value in useLocations hook inside a useMemo like this:
export default function useLocations(locationsToMatch) {
const state = useAnotherHookToGetStateFromStore();
const { allStores } = state.locations;
const allLocations = {};
const getStores = () => {
allStores.forEach((store) => {
const { locationId, locationType } = store;
const isLocationPresent = locationsToMatch.indexOf(locationId) !== -1;
if (isLocationPresent && locationType === 'someValue') {
allLocations[locationId] = true;
} else {
allLocations[locationId] = false;
}
});
return allLocations;
};
return useMemo(() => getStores(), [locationsToMatch, state]);
}
But this still causes infinite re-rendering of the consuming component. So how can I return a memoized value from my custom hook useLocations to prevent infinite re-rendering?
I'm wondering which of these two approaches is best for handling state when using custom hooks in react.
Having read this article on writing testable react hooks I've started having a custom hook associated with a component to act as a single entry point for the state and business logic.
However, it has made me wonder which approach is best for handling state. Should I keep lots of state values and pass the setState action to other custom hooks or should I return a value and let the more specific hook handle the more specific state? I've made a basic example to illustrate what I'm trying to articulate!
// useMyComponent.ts
// Approach 1
export default (): {
foo: string;
} => {
const [foo, setFoo] = useState < string | undefined>();
// State contained within this custom hook
const [authorisedInfo, setAuthorisedInfo] = useState<string | undefined>();
const [googleToken, setGoogleToken] = useState<string | undefined>();
useEffect(() => {
// setState passed down and set within a different custom hook
useSignInWithGoogle(setGoogleToken);
}, []);
useEffect(() => {
if (!googleToken) return;
usePerformAuthCall(setAuthorisedInfo);
}, [googleToken]);
useEffect(() => {
if (!authorisedInfo || !googleToken) return;
const { foo } = doSomethingHere(authorisedInfo, googleToken);
setFoo(foo);
}, [authorisedInfo, googleToken]);
return { foo };
};
// Approach 2
export default (): {
foo: string;
} => {
const [foo, setFoo] = useState < string | undefined>();
useEffect(() => {
// state value passed back from the custom hook
const { googleToken } = useSignInWithGoogle();
}, []);
useEffect(() => {
if (!googleToken) return;
const { authorisedInfo } = usePerformAuthCall(setAuthorisedInfo);
}, [googleToken]);
useEffect(() => {
if (!authorisedInfo || !googleToken) return;
const { foo } = doSomethingHere(authorisedInfo, googleToken);
setFoo(foo);
}, [authorisedInfo, googleToken]);
return { foo };
};
Thanks for the help!
I have a Provider that provides a state variable and its corresponding setter through two contexts.
const BookedBatchContext = createContext({})
const SetBookedBatchContext = createContext(null)
const initialState = {
id: null
}
The Provider looks like this:
export const BookedBatchProvider = ({ children }: { children: any }) => {
const [bookedBatch, setBookedBatch] = useState(localState ||initialState)
return (
<SetBookedBatchContext.Provider value={setBookedBatch}>
<BookedBatchContext.Provider value={bookedBatch}>
{ children }
</BookedBatchContext.Provider>
</SetBookedBatchContext.Provider>
)
}
Through a custom hook I make the setBookedBatch available to other components:
export const useBookedBatch = () => {
const bookedBatch = useContext(BookedBatchContext)
const setBookedBatch = useContext(SetBookedBatchContext)
return { bookedBatch, setBookedBatch }
}
When trying to use the setBookedBatch function, I get following error in a given component:
setBookedBatch(selectedBatch)
The error:
TS2721: Cannot invoke an object which is possibly 'null'.
Since the setter of the useState function is a function that I didn't create, I don't know how to initialise it when I create the context:
const SetBookedBatchContext = createContext(null)
So that TypeScript does not complain.
How can I know the initial value of the setter function?
How can I avoid that TS complains about the null value, if I am not providing any types?
The return types of React.createContext and React.useState are determined by inference from the initial values you pass in.
1.) You can create the proper context type by manually specifying the generic type:
const SetBookedBatchContext = createContext<null | React.Dispatch<React.SetStateAction<State>>>(null)
Note: The setter for useState has type React.Dispatch<React.SetStateAction<State>>, where State is whatever localState || initialState is.
2.) Assert in your custom Hook useBookedBatch, that setBookedBatch is not null:
export const useBookedBatch = () => {
const bookedBatch = useContext(BookedBatchContext)
const setBookedBatch = useContext(SetBookedBatchContext)
if (setBookedBatch === null) throw new Error() // this will make setBookedBatch non-null
return { bookedBatch, setBookedBatch }
// returns: { bookedBatch: {}; setBookedBatch: React.Dispatch<React.SetStateAction<State>>; }
}
3.) Then setBookedBatch can be invoked without assertion later on:
const App = () => {
const { setBookedBatch } = useBookedBatch()
useEffect(() => { setBookedBatch({ id: "foo" }) }, [])
}
Sample on the playground