Export function from inside a React function - reactjs

I have a function that handles React Native location. For demonstration:
const useLocation = () => {
const [fetchingLocation, setFetchingLocation] = useState(true);
...
const changeSystemPermissions = useCallback(() => {...});
useEffect(() => {
//does many things
}, [...])
}
I need to have the function changeSystemPermissions inside useLocation as it uses the state.
I realize that I can export the changeSystemPermissions function as a const with a return [changeSystemPermissions, ...] and then import it in another component with:
const [
changeSystemPermissions,
...
] = useLocation();
However, it will ALSO run the useEffect function. I do want it to run once, but I need to access changeSystemPermissions in several other components and I don't want the useEffect to run multiple times.
I was thinking I will just take out the changeSystemPermissions function outside of useLocation, but it needs to use the state. I suppose I COULD pass the state vars into the changeSystemPermissions when it is outside useLocation, but that would be verbose and ugly.
How can I export changeSystemPermissions and just that function without having to import the whole useLocation function?

Can you move the useEffect to the component one ?
const useLocation = () => {
const [fetchingLocation, setFetchingLocation] = useState(true);
const changeSystemPermissions = useCallback(() => {
...
});
const funcToExecute = useCallback(() => {
....
}, []);
return { changeSystemPermissions, funcToExecute }
}
And put it in the component :
const {
changeSystemPermissions,
funcToExecute,
} = useLocation();
useEffect(() => {
funcToExecute()
}, [...])
Also, if you really need the useEffect to be in the custom hook,
maybe you can add a param to this hook.
const useLocation = (shouldTriggerEffect) => {
const [fetchingLocation, setFetchingLocation] = useState(true);
const changeSystemPermissions = useCallback(() => {
...
});
useEffect(() => {
if (shouldTriggerEffect) {
...
}
}, [shouldTriggerEffect])
return { changeSystemPermissions, funcToExecute }
}
And then in the component,
const {
changeSystemPermissions,
} = useLocation(false);
Tell me if I misunderstood something or if it helps :)

When ever you call a hook inside a React functional component, it will create a new state for that hook and not sharing among components. But there is a library which could help you achieve that:
https://github.com/betula/use-between
You could follow example to use this library or maybe just read the code and utilize the approach for your case to share the hook state between components.

Related

How can I call a custom hook which accepts parameter inside useEffect?

In my component I use a function which I want to extract, it uses some hooks for setting url params. I created a custom hook.
function useMyCustomHook() {
const history = useHistory();
const location = useLocation();
const locationParams = new URLSearchParams(location.search);
function myCustomHook(id: string ) {
does something
}
return myCustomHook;
}
So I extracted it like shown above and now I want to use it in my other component and inside a useEffect hook.
const { myCustomHook } = useMyCustomHook(); // It asks me to add parameters,
but I want to pass it inside useEffect
useEffect(() => {
if (something) myCustomHook(myParam);
}, [foo]);
Is this a possible approach? Or is there a better solution where I can extract something with hooks and then reuse it in useEffect with parameters? Thank you!
First you need export your custom Hook, I think if you need return a function with id, that function need be executed each id change.
custom hook
import { useCallback} from "react"
import {useNavigate, useLocation} from "react-router-dom"
export const useMyCustomHook = (id) => {
const navigate = useNavigate() // React-router v6
const location = useLocation()
const locationParams = new URLSearchParams(location.search)
const anyFunction = useCallback(() => {
// does something
}, [id]) // the function execute if "id" change
return anyFunction
}
where you wanna use your custom hook
import {useMyCustomHook} from "your router"
const {anyFunction} = useMyCustomHook(id) // your pass the id
useEffect(() => {
if (something) anyFunction()
}, [foo])
I think this is the better way. useCallback only render the function of the params change.

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

React context provider not setting value on page load/refresh

I have the following hook for Pusher and I use it to share one instance across the application.
import React, { useContext, useEffect, useRef } from "react";
import Pusher from "pusher-js";
const PusherContext = React.createContext<Pusher | undefined>(undefined);
export const usePusher = () => useContext(PusherContext)
export const PusherProvider: React.FC = (props) => {
const pusherRef = useRef<Pusher>();
useEffect(() => {
pusherRef.current = new Pusher(PUSHER_APP_KEY, {
cluster: 'eu'
})
return () => pusherRef.current?.disconnect()
}, [pusherRef]);
return (
<PusherContext.Provider value={pusherRef.current}>
{props.children}
</PusherContext.Provider>
)
}
The problem is that the provider always has an undefined value on page refresh/load. But when I trigger a re-render the value is correctly set. I would like to have the instance without the need of re-rendering.
Why is this happening?
I believe you can use the next construction:
export const PusherProvider = (props) => {
const pusher = useMemo(() => new Pusher(APP_PUSHER_KEY, { cluster: 'eu' }), [])
useEffect(() => () => pusher.disconnect(), [pusher])
return <PusherContext.Provider value={pusher}>{props.children}</PusherContext.Provider>
}
I have solved this issue by following way.
If you set State/const data inside "useEffect" will not work. as that will not run when page refresh but the state declaration those are outside the "useEffect" will run. Hence it will reset default values.
So I resolved by setting the state/const value outside of "useEffect" and done.

How can I re-fetch an API using react hooks

devs,
I have decided to finally learn react hooks with what I thought would be a simple project. I can't quite figure out how I re-fetch an API using react hooks. Here is the code I have so far.
import React, { useState, useEffect } from "react"
import useFetch from "./utils/getKanya"
const kanye = "https://api.kanye.rest"
const Index = () => {
let [kanyaQuote, setKanyeQuote] = useState(null)
let data = useFetch(kanye)
const getMore = () => {
setKanyeQuote(useFetch(kanye))
}
return (
<>
<h1>Welcome to Next.js!</h1>
<p>Here is a random Kanye West quote:</p>
{!data ? <div>Loading...</div> : <p>{!kanyaQuote ? data : kanyaQuote}</p>}
<button onClick={getMore}>Get new quote</button>
</>
)
}
export default Index
I get the kanyeQuote state value to null
I fetch the initial data
I either show "Loading..." or the initial quote
I am trying to set up a button to re-fetch the API and store the data in kanyeQuote via getKanyeQuote (setState)
This is the error I get Error: Invalid hook call...
I would greatly appreciate any guidance you can provide on this.
The issue here is, that you can only use hooks directly inside the root of your component.
It's the number 1 'rule of hooks'. You can read more about that here
const getMore = () => {
setKanyeQuote(useFetch(kanye) /* This cannot work! */)
}
There are a few ways you could work around that. Without knowing the internal logic in your useFetch-hook I can only assume you are able to change it.
Change hook to handle its state internally
One way to work around that would be to change the logic of your custom useFetch hook to provide some form of function that fetches the data and updates the state internally. It could then look something like this:
const { data, doFetch } = useFetch(kanye);
useEffect(() => {
doFetch(); // initialFetch
}, []);
const getMore = () => {
doFetch();
};
// ...
You would then need to change the internal logic of your useFetch-hook to use useState internally and expose the getter of it. It would look something like this:
export const useFetch = (url) => {
const [data, setData] = useState(null);
const doFetch = () => {
// Do your fetch-Logic
setData(result);
};
return { data, doFetch };
};
Change hook not to handle any state at all.
If you only want to manage the state of the loaded data in the parent component, you could just provide the wrapped fetch function through the hook; Something like that:
const doFetch = useFetch(kanye);
const [data, setData] = useState(null);
useEffect(() => {
setData(doFetch()); // initialFetch
}, []);
const getMore = () => {
setData(doFetch())
};
// ...
You would then need to change the internal logic of your useFetch-hook to not have any internal state and just expose the wrapped fetch. It would look something like this:
export const useFetch = (url) => {
const doFetch = () => {
// Do your fetch-Logic
return result;
};
return doFetch;
};

Call a Redux Action inside a useEffect

My objective here is to call an action inside a useEffect.
const ShowTodos = (props) =>{
useEffect(()=>{
props.fetchTodos()
},[])
}
const mapStateToProps = (state)=>{
return {
todos:Object.values(state.todos),
currentUserId:state.authenticate.userId
}
}
export default connect(mapStateToProps,{fetchTodos})(ShowTodos)
It works fine but I got a warning
React Hook useEffect has a missing dependency: 'props'. Either include it or remove the dependency array react-hooks/exhaustive-deps.
But if I'm going to add props as my second parameter in my useEffects then it will run endlessly.
My first workaround here is to use the useRef but it seems that it will always re-render thus re-setup again the useRef which I think is not good in terms of optimization.
const ref = useRef();
ref.current = props;
console.log(ref)
useEffect(()=>{
ref.current.fetchTodos()
},[])
Is there any other workaround here ?
That is an eslint warning that you get if any of the dependency within useEffect is not part of the dependency array.
In your case you are using props.fetchTodos inside useEffect and the eslint warning prompts you to provide props as a dependency so that if props changes, the useEffect function takes the updated props from its closure.
However since fetchTodos is not gonna change in your app lifecycle and you want to run the effect only once you can disable the rule for your case.
const ShowTodos = (props) =>{
const { fetchTodos } = props
useEffect(()=>{
fetchTodos()
// eslint-disable-next-line import/no-extraneous-dependencies
},[])
}
const mapStateToProps = (state)=>{
return {
todos:Object.values(state.todos),
currentUserId:state.authenticate.userId
}
}
export default connect(mapStateToProps,{fetchTodos})(ShowTodos)
You however can solve the problem without disabling the rule like
const ShowTodos = (props) =>{
const { fetchTodos } = props
useEffect(()=>{
fetchTodos()
},[fetchTodos])
}
I however will recommend that you know when exactly should you disable the rule or pass the values to the dependency array.
You have to add fetchTodos to dependencies.
const ShowTodos = ({ fetchTodos }) => {
useEffect(() => {
fetchTodos();
}, [fetchTodos])
...
}
or like this.
const ShowTodos = (props) => {
const { fetchTodos } = props;
useEffect(() => {
fetchTodos();
}, [fetchTodos])
...
}

Resources