How to write React hooks with called functions? - reactjs

I have a useQuery() hook:
const useQuery = () => {
const router = useRouter();
const build = (query) => {}
const read = () => {}
}
export default useQuery;
I want to be able to use hook in my code like:
const query = useQuery();
useEffect(()=>{
const oldQuery = query.read();
if(oldQuery.length === 0) query.build(newQuery);
},[])
but every time I try to call query.read() I get error Property 'read' does not exist on type 'void'. It looks like scope issue. How can I fix it?

You need a return statement in useQuery:
const useQuery = () => {
const router = useRouter();
const build = (query) => {}
const read = () => {}
return { router, build, read }
}
You may also want to consider memoizing these functions, so that code which consumes this hook doesn't do unnecessary work because it thinks the functions have changed:
const build = useCallback((query) => {}, []);
const read = useCallback((() => {}, []);

Related

Invalid Hook Call for custom hook

I have written a function for API calls. I want to reuse this function from a different page.
FetchData.js
export const FetchData = (url, query, variable) => {
const [fetchData, setFetchData] = useState([]);
useEffect(() => {
const fetchData = async () => {
const queryResult = await axios.post(
url, {
query: query,
variables: variable
}
)
const result = queryResult.data.data;
setFetchData(result.mydata)
};
fetchData();
})
return {fetchData, setFetchData}
}
Here is my main page from where I am trying to call the API using the following code
mainPage.js
import { FetchData } from './FetchData'
export const MainPage = props => {
const onClick = (event) => {
const {fetchData, setFetchData} = FetchData(url, query, variable)
console.log(fetchData)
}
}
It is returning the following error -
Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
If you need to fetch data on response to an event, you don't need a useEffect.
const useData = (url, query, variable) => {
const [data, setData] = useState([]);
const fetchData = async () => {
const queryResult = await axios.post(url, {
query: query,
variables: variable,
});
setData(queryResult.data.data);
};
return {data, fetchData}
};
export const MainPage = (props) => {
const {data, fetchData} = useData(url, query, variable);
const onClick = (event) => {
fetchData()
};
};
Hooks can't be used inside handler functions.
Do this instead:
import { FetchData } from './FetchData'
export const MainPage = props => {
const {fetchData, setFetchData} = FetchData(url, query, variable)
const onClick = (event) => {
console.log(fetchData)
}
}

useEffect in a custom hook freezes my react-native app when the hook is used in more than one place

I have created a custom hook that fetches setting from an api that uses Async-Storage.
// Takes the key/name of the setting to retrieve
export const useSettings = (key) => {
// stores the json string result in the setting variable
const [setting, setSetting] = useState("");
const deviceStorage = useContext(DeviceStorageContext);
useEffect(() => {
getValue()
.then(value => setSetting(value));
}, []);
// gets value from my custom api that uses Async-Storage to handle r/w of the data.
const getValue = async () => await deviceStorage.getValueStored(key);
const setValue = async (value) => {
await deviceStorage.setValueStored(key, value);
getValue().then(value => setSetting(value));
};
const removeValue = async () => { }
return [setting, { setValue,removeValue }];
};
This works as expected in Main.jsx without any problem.
const Main = () => {
const [units, operations] = useSettings('units');
useEffect(() => {
const initSettings = async () => {
if (units) {
console.log(units)
return;
}
await operations.setValue({ pcs: 1, box: 20 });
};
initSettings();
}, []);
However, when I even just call the useSetting hook in Form.jsx and visit the page, it freezes my entire app to just that page.
const FormView = ({ handleReset, handleSubmit }) => {
const [setting,] = useSettings('units');
Removing the useState and useEffect fixes it and calling these methods directly works but I really don't want to call getValue() throughout my project and use async/await code to handle it.
Stuck on this for hours now. Any help will be appreciated.
Thank you.
It was a dropdown component library inside FormView that was messing it up. Removing that library fixed it.

React Hook useEffect has a missing dependency for redux action as parameters

I found many similar questions here about React Hook useEffect has a missing dependency. I have already checked them, but I didn't find solutions as I faced. I want to pass redux thunk function as a parameter to React custom hook.
Below is my code and it is working fine. But, I got dependency missing warning, I don't want to add ignore warning eslint. If I add dispatchAction to dependency array list, it is dispatching again and again because redux thunk asyn function has fulfilled, reject, pending.
Custom Hook
const useFetchData = (dispatchAction, page) => {
const dispatch = useDispatch();
const [loadMoreLoading, setLoadMoreLoading] = useState(false);
const [errorMsg, setErrorMsg] = useState();
useEffect(() => {
const fetchData = async () => {
setLoadMoreLoading(true);
const resultAction = await dispatch(dispatchAction);
if (resultAction.meta.requestStatus === 'rejected') {
setErrorMsg(resultAction.payload.message);
}
setLoadMoreLoading(false);
};
fetchData();
}, [dispatch, page]);
return [loadMoreLoading, errorMsg]; // it is asking for adding dispatchAction.
My component
const SomeListing = ({userId}) => {
const [page, setPage] = useState(1);
const [loadMoreLoading, errorMsg] = useFetchData(
fetchPropertyByUserId({userId: userId, page: page}),
page,
);
}
So, is there any way to be able to add redux thunk function in react custom hook?
The function fetchPropertyByUserId, when called i.e. fetchPropertyByUserId({userId: userId, page: page}), returns an "actionCreator" function.
Hence, when you call this function at the place of first parameter of your hook useFetchData, it returns a new "actionCreator" function each time (we know that hooks are called at each render):
In SomeListing.jsx:
const [loadMoreLoading, errorMsg] = useFetchData(
fetchPropertyByUserId({userId: userId, page: page}), // <-- Here: it returns a new "actionCreator" function at call (render)
page,
);
And, as soon as you put this function (first parameter of the hook i.e. dispatchAction) as a dependency of useEffect, it should cause an infinite execution of the effect because, now we know, that dispatchAction is getting created (hence, changed) at every render.
In useFetchData.js:
export const useFetchData = (dispatchAction, page) => {
// ...
useEffect(() => {
const fetchData = async () => {
setLoadMoreLoading(true)
const resultAction = await dispatch(dispatchAction)
if (resultAction.meta.requestStatus === 'rejected') {
setErrorMsg(resultAction.payload.message)
}
setLoadMoreLoading(false)
}
fetchData()
}, [dispatch, dispatchAction, page]) // <-- "dispatchAction" added here
// ...
How to fix it?
Pass a memoized actionCreator function:
In SomeListing.jsx:
export const SomeListing = ({ userId }) => {
const [page, setPage] = useState(1)
// Here: "fetchPropertyByUserIdMemo" is memoized now
const fetchPropertyByUserIdMemo = useMemo(
() => fetchPropertyByUserId({ userId: userId, page: page }),
[page, userId]
)
const [loadMoreLoading, errorMsg] = useFetchData(fetchPropertyByUserIdMemo, page)
// ...
}
How about extracting the fetch method from useEffect?:
const fetchData = async () => {
setLoadMoreLoading(true);
const resultAction = await dispatch(dispatchAction);
if (resultAction.meta.requestStatus === 'rejected') {
setErrorMsg(resultAction.payload.message);
}
setLoadMoreLoading(false);
};
useEffect(() => {
fetchData();
}, [fetchData]);

Access the state from custom hook in another

I have a custom hook that reads and parses JSON object from the DOM like so:
export const useConfig = () => {
const [config, setConfig] = useState<Config>();
useEffect(() => {
if (document) {
const config = document.getElementById('some-dom-element');
setConfig(JSON.parse(config?.innerHTML || '{}'));
}
}, []);
return config;
};
This object has some meaningful information for different concerns in my application.
So I created another hook that just gets the objects array from the config object:
export const useOptions = () => {
const [options, setOptions] = useState<Options>();
const config = useConfig();
useLayoutEffect(() => {
setOptions(config?.options);
}, [options]);
return options;
};
This is ok, but if I only want to read from the DOM once then I would need to call my useOptions hook at the top level of my component tree and pass the result down to so that components further down the tree can access it.
Is there way that I can change my useOptions hook so that I can call it only in the function components that need it, and not have the DOM read by useConfig on every call?
EDIT: I could probably use Recoil as a lightweight state management solution, but I was wondering if there's a better way to write these hooks so that I don't need to add additional deps
You are describing a custom hook that acts as a singleton.
Try using reusable library, the implementation is simple so you can use it as a recipe:
const useCounter = createStore(() => {
const [counter, setCounter] = useState(0);
return {
counter,
increment: () => setCounter(prev => prev + 1)
}
});
const Comp1 = () => {
const something = useCounter();
}
const Comp2 = () => {
const something = useCounter(); // same something
}
const App = () => (
<ReusableProvider> {/* no initialization code, stores automatically plug into the top provider */}
...
</ReusableProvider>
)
So for your use case, useConfig probably should be in the global store:
const useConfig = {...};
export default createStore(useConfig);
// Always the same config
const config = useConfig();

Why my custom hook causes infinite data refetching?

My component gets the hashed-id from the query string, then calls api with that hash to fetch a post for review.
eslint forces me to add my custom hook to dependency array.
fetchpost();
}, [query]);
But doing this causes an infinite loop. In order to stop it I need to disable this eslint rule, as seen below.
// component file
const history = useHistory();
const dispatch = useDispatch();
const query = useQuery();
const [post, setPost] = useState(null);
const [hash, setHash] = useState(null);
useEffect(() => {
const fetchpost = async () => {
const hash = query.get("hashed_id");
const post = await fetchReviewPost(
`/api/posts/${hash}/review`
);
setHash(hash);
setPost(post);
};
fetchpost();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// utils file
import { useLocation } from "react-router-dom";
export const getCurrentURL = () => {
return document.URL;
};
export const useQuery = () => {
const queryString = useLocation().search;
return new URLSearchParams(queryString);
};
Dan Abramov writes An infinite loop may also happen if you specify a value that always changes in the dependency array.
Is that the case here? Is query reference different on every render? And why eslint wants to put it in a dependency array?
He also says removing a dependency you use (or blindly specifying []) is usually the wrong fix. Which I sort of did by disabling the eslint rule.
Any thoughts?
If you really want to keep sticking to eslint suggestions and using the useQuery hook, here is an alternative way:
// component file
const history = useHistory();
const dispatch = useDispatch();
const q = useQuery();
const [query] = useState(q);
const [post, setPost] = useState(null);
const [hash, setHash] = useState(null);
useEffect(() => {
const fetchpost = async () => {
const hash = query.get("hashed_id");
const post = await fetchReviewPost(
`/api/posts/${hash}/review`
);
setHash(hash);
setPost(post);
};
fetchpost();
}, [query]);
At this point the query value keeps constant across the subsequent function calls.
However, I'd remove the useQuery hook, and place its content straight into the fetchpost function.

Resources