I'm developing a Raycast extension.
In this function, I need to create or update a database before querying it:
import { useSQL } from "#raycast/utils";
export const useSqlNote = <NoteItem>(query: string) => {
const [ready, setReady] = useState<boolean>(false);
let theData: NoteItem[] = [];
let loadingSelect = true;
let permissionVw;
useEffect(() => {
(async () => {
await create_or_update_db();
setReady(true);
})();
}, [query]);
try {
const { data, isLoading, permissionView } = useSQL<NoteItem>(PATH, query);
if (ready) {
theData = data || [];
loadingSelect = isLoading;
permissionVw = permissionView;
}
} catch (e) {}
return { data: theData, isLoading: loadingSelect, errorView: permissionVw };
};
When the database is already created, no pb.
But when it needs to be created, the process takes several ms -- and of course the call of the hook useSQL raises an error, but it should be handled.
However, I'm getting this error:
Warning: React has detected a change in the order of Hooks called by Command. ...
Error: Rendered more hooks than during the previous render.
Any idea on how to fix it?
You are not calling the useQuery on the top level of the component. Remove the try catch block.
To conditionally call the useQuery hook, the documentation for raycast useQuery states that you can pass the options with execute param to skip the calling of the query.
"options.execute is a boolean to indicate whether to actually execute the function or not. This is useful for cases where one of the function's arguments depends on something that might not be available right away (for example, depends on some user inputs). Because React requires every hook to be defined on the render, this flag enables you to define the hook right away but wait until you have all the arguments ready to execute the function."
const { data, isLoading, permissionView } = useSQL<NoteItem>(PATH, query, {execute: ready});
//You do not need this
//if (ready) {
// theData = data || [];
// loadingSelect = isLoading;
// permissionVw = permissionView;
// }
return { data: data || [], isLoading, errorView: permissionView };
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 an object which value updates and i would like to know if there is a way to re-render the component when my object value is updated.
I can't create a state object because the state won't be updated whenever the object is.
Using a ref is not a good idea(i think) since it does not cause a re-render when updated.
The said object is an instance of https://docs.kuzzle.io/sdk/js/7/core-classes/observer/introduction/
The observer class doesn't seem to play well with your use case since it's just sugar syntax to manage the updates with mutable objects. The documentation already has a section for React, and I suggest following that approach instead and using the SDK directly to retrieve the document by observing it.
You can implement this hook-observer pattern
import React, { useCallback, useEffect, useState } from "react";
import kuzzle from "./services/kuzzle";
const YourComponent = () => {
const [doc, setDoc] = useState({});
const initialize = useCallback(async () => {
await kuzzle.connect();
await kuzzle.realtime.subscribe(
"index",
"collection",
{ ids: ["document-id"] },
(notification) => {
if (notification.type !== "document" && notification.event !== "write")
return;
// getDocFromNotification will have logic to retrieve the doc from response
setDoc(getDocFromNotification(notification));
}
);
}, []);
useEffect(() => {
initialize();
return () => {
// clean up
if (kuzzle.connected) kuzzle.disconnect();
};
}, []);
return <div>{JSON.stringify(doc)}</div>;
};
useSyncExternalStore, a new React library hook, is what I believe to be the best choice.
StackBlitz TypeScript example
In your case, a simple store for "non state object" is made:
function createStore(initialState) {
const callbacks = new Set();
let state = initialState;
// subscribe
const subscribe = (cb) => {
callbacks.add(cb);
return () => callbacks.delete(cb);
};
// getSnapshot
const getSnapshot = () => state;
// setState
const setState = (fn) => {
state = fn(state);
callbacks.forEach((cb) => cb());
};
return { subscribe, getSnapshot, setState };
}
const store = createStore(initialPostData);
useSyncExternalStore handles the job when the update of "non state object" is performed:
const title = React.useSyncExternalStore(
store.subscribe,
() => store.getSnapshot().title
);
In the example updatePostDataStore function get fake json data from JSONPlaceholder:
async function updatePostDataStore(store) {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${Math.floor(Math.random()*100)+1}`)
const postData = await response.json()
store.setState((prev)=>({...prev,...postData}));
};
My answer assumes that the object cannot for some reason be in React as state (too big, too slow, too whatever). In most cases that's probably a wrong assumption, but it can happen.
I can't create a state object because the state won't be updated whenever the object is
I assume you mean you can't put that object in a React state. We could however put something else in state whenever we want an update. It's the easiest way to trigger a render in React.
Write a function instead of accessing the object directly. That way you can intercept every call that modifies the object. If you can reliably run an observer function when the object changes, that would work too.
Whatever you do, you can't get around calling a function that does something like useState to trigger a render. And you'll have to call it in some way every time you're modifying the object.
const myObject = {};
let i = 0;
let updater = null;
function setMyObject(key, value) {
myObject[key] = value;
i++;
if (updater !== null) {
updater(i);
}
};
Change your code to access the object only with setMyObject(key, value).
You could then put that in a hook. For simplicity I'll assume there's just 1 such object ever on the page.
function useCustomUpdater() {
const [, setState] = useState(0);
useEffect(()=>{
updater = setState;
return () => {
updater = null;
}
}, [setState]);
}
function MyComponent() {
useCustomUpdater();
return <div>I re-render when that object changes</div>;
}
Similarly, as long as you have control over the code that interacts with this object, you could wrap every such call with a function that also schedules an update.
Then, as long as your code properly calls the function, your component will get re-rendered. The only additional state is a single integer.
The question currently lacks too much detail to give a good assessment whether my suggested approach makes sense. But it seems like a very simple way to achieve what you describe.
It would be interesting to get more information about what kind of object it is, how frequently it's updated, and in which scope it lives.
We have written a custom data fetching hook useInternalApi which is similar to the useDataApi hook at the very bottom of this fairly decent tutorial on data fetching with react hooks. Our app fetches a lot of sports data, and in particular, we are trying to figure out the right data-fetching pattern for our use case, which is fairly simple:
Fetch general info for a specific entity (an NCAA conference, for example)
Use info returned with that entity (an array of team IDs for teams in the specific conference), and fetch info on each team in the array.
For this, our code would then look something like this:
import `useInternalApi` from '../path-to-hooks/useInternalApi';
// import React... and other stuff
function ComponentThatWantsTeamInfo({ conferenceId }) {
// use data fetching hook
const [conferenceInfo, isLoading1, isError1] = useInternalApi('conferenceInfo', { conferenceId: conferenceId })
// once conferenceInfo loads, then load info from all teams in the conference
if (conferenceInfo && conferenceInfo.teamsArray) {
const [teamInfos, isLoading2, isError2] = useInternalApi('teamInfo', { teamIds: conferenceInfo.teamIds })
}
}
In the example above, conferenceId is an integer, teamIds is an array of integers, and the combination of the 2 parameters to the useInternalApi function create a unique endpoint url to fetch data from. The two main problems with this currently are:
Our useInternalApi hook is called in an if statement, which is not allowed per #1 rule of hooks.
useInternalApi is currently built to only make a single fetch, to a specific endpoint. Currently, it cannot handle an array of teamIds like above.
What is the correct data-fetching pattern for this? Ideally, teamInfos would be an object where each key is the teamId for one of the teams in the conference. In particular, is it better to:
Create a new internal hook that can handle an array of teamIds, will make the 10 - 20 fetches (or as many as needed based on the length of the teamsArray), and will use Promise.all() to return the results all-together.
Keep the useInternalApi hook as is, and simply call it 10 - 20 times, once for each team.
Edit
I'm not sure if the underlying code to useInternalApi is needed to answer this question. I try to avoid creating very long posts, but in this instance perhaps that code is important:
const useInternalApi = (endpoint, config) => {
// Set Data-Fetching State
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
// Use in lieu of useEffect
useDeepCompareEffect(() => {
// Token/Source should be created before "fetchData"
let source = axios.CancelToken.source();
let isMounted = true;
// Create Function that makes Axios requests
const fetchData = async () => {
// Set States + Try To Fetch
setIsError(false);
setIsLoading(true);
try {
const url = createUrl(endpoint, config);
const result = await axios.get(url, { cancelToken: source.token });
if (isMounted) {
setData(result.data);
}
} catch (error) {
if (isMounted) {
setIsError(true);
}
} finally {
if (isMounted) {
setIsLoading(false);
}
}
};
// Call Function
fetchData();
// Cancel Request / Prevent State Updates (Memory Leaks) in cleanup function
return () => {
isMounted = false; // set to false to prevent state updates / memory leaks
source.cancel(); // and cancel the http request as well because why not
};
}, [endpoint, config]);
// Return as length-3 array
return [data, isLoading, isError];
};
In my opinion, if you need to use a hook conditionally, you should use that hook inside of a separate component and then conditionally render that component.
My understanding, correct me if I'm wrong, is that the initial API call returns an array of ids and you need to fetch the data for each team based on that id?
Here is how I'd do something of that sorts.
import `useInternalApi` from '../path-to-hooks/useInternalApi';
// import React... and other stuff
function ComponentThatDisplaysASpecificTeam(props){
const teamId = props.teamId;
const [teamInfo] = useInternalApi('teamInfo', { teamId });
if(! teamInfo){
return <p>Loading...</p>
}
return <p>do something with teamInfo...</p>
}
function ComponentThatWantsTeamInfo({ conferenceId }) {
// use data fetching hook
const [conferenceInfo, isLoading1, isError1] = useInternalApi('conferenceInfo', { conferenceId: conferenceId })
if (! conferenceInfo || ! conferenceInfo.teamsArray) {
return <p>this is either a loading or an error, you probably know better than me.</p>
}
// Let the data for each team be handled by its own component. This also lets you not have to use Promise.all
return (
<div>
{conferenceInfo.teamIds.map(teamId => (
<ComponentThatDisplaysASpecificTeam teamId={teamId} />
))}
</div>
)
}
I have a large JSON blob stored inside my Context that I can then make references to using jsonpath (https://www.npmjs.com/package/jsonpath)
How would I go about being able to access the context from inside useEffect() without having to add my context variable as a dependency (the context is updated at other places in the application)?
export default function JsonRpc({ task, dispatch }) {
const { data } = useContext(DataContext);
const [fetchData, setFetchData] = useState(null);
useEffect(() => {
task.keys.forEach(key => {
let val = jp.query(data, key.key)[0];
jp.value(task.payload, key.result_key, val);
});
let newPayload = {
jsonrpc: "2.0",
method: "call",
params: task.payload,
id: "1"
};
const domain = process.env.REACT_APP_WF_SERVER;
let params = {};
if (task.method === "GET") {
params = newPayload;
}
const domain_params =
JSON.parse(localStorage.getItem("domain_params")) || [];
domain_params.forEach(e => {
if (e.domain === domain) {
params[e.param] = e.value;
}
});
setFetchData({ ...task, payload: newPayload, params: params });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [task]);
}
I'm gonna need to post an answer because of code, but I'm not 100% sure about what you need, so I'll build a correct answer with your feedback :)
So, my first idea is: can't you split your effects in two React.useEffect? Something like this:
export default function JsonRpc({ task, dispatch }) {
...
useEffect(() => {
...
setFetchData(...);
}, [task]);
useEffect(() => {
...
}, [data]);
..
}
Now, if my understanding are correct, this is an example of events timeline:
Due to the update on task you will trigger the first useEffect, which can setFetchData();
Due to the update on fetchData, and AXIOS call is made, which updates data (property in the context);
At this, you enter the second useEffect, where you have the updated data, but NO call to setFetchData(), thus no loop;
Then, if you wanted (but couldn't) put data in the dependencies array of your useEffect, I can imagine the two useEffect I wrote have some shared code: you can write a common method called by both useEffects, BUT it's important that the setFetchData() call is outside this common method.
Let me know if you need more elaboration.
thanks for your reply #Jolly! I found a work around:
I moved the data lookup to a state initial calculation:
const [fetchData] = useState(processFetchData(task, data));
then im just making sure i clear the component after the axios call has been made by executing a complete function passed to the component from its parent.
This works for now, but if you have any other suggestions id love to hear them!
I'm hoping someone can explain to me the correct usage of React hook in this instance, as I can't seem to find away around it.
The following is my code
useEffect(() => {
_getUsers()
}, [page, perPage, order, type])
// This is a trick so that the debounce doesn't run on initial page load
// we use a ref, and set it to true, then set it to false after
const firstUpdate = React.useRef(true);
const UserSearchTimer = React.useRef()
useEffect(() => {
if(firstUpdate.current)
firstUpdate.current = false;
else
_debounceSearch()
}, [search])
function _debounceSearch() {
clearTimeout(UserSearchTimer.current);
UserSearchTimer.current = setTimeout( async () => {
_getUsers();
}, DEBOUNCE_TIMER);
}
async function _getUsers(query = {}) {
if(type) query.type = type;
if(search) query.search = search;
if(order.orderBy && order.order) {
query.orderBy = order.orderBy;
query.order = order.order;
}
query.page = page+1;
query.perPage = perPage;
setLoading(true);
try {
await get(query);
}
catch(error) {
console.log(error);
props.onError(error);
}
setLoading(false);
}
So essentially I have a table in which i am displaying users, when the page changes, or the perPage, or the order, or the type changes, i want to requery my user list so i have a useEffect for that case.
Now generally I would put the _getUsers() function into that useEffect, but the only problem is that i have another useEffect which is used for when my user starts searching in the searchbox.
I don't want to requery my user list with each and every single letter my user types into the box, but instead I want to use a debouncer that will fire after the user has stopped typing.
So naturally i would create a useEffect, that would watch the value search, everytime search changes, i would call my _debounceSearch function.
Now my problem is that i can't seem to get rid of the React dependency warning because i'm missing _getUsers function in my first useEffect dependencies, which is being used by my _debounceSearch fn, and in my second useEffect i'm missing _debounceSearch in my second useEffect dependencies.
How could i rewrite this the "correct" way, so that I won't end up with React warning about missing dependencies?
Thanks in advance!
I would setup a state variable to hold debounced search string, and use it in effect for fetching users.
Assuming your component gets the query params as props, it would something like this:
function Component({page, perPage, order, type, search}) {
const [debouncedSearch, setDebouncedSearch] = useState(search);
const debounceTimer = useRef(null);
// debounce
useEffect(() => {
if(debounceTime.current) {
clearTimeout(UserSearchTimer.current);
}
debounceTime.current = setTimeout(() => setDebouncedSearch(search), DEBOUNCE_DELAY);
}, [search]);
// fetch
useEffect(() => {
async function _getUsers(query = {}) {
if(type) query.type = type;
if(debouncedSearch) query.search = debouncedSearch;
if(order.orderBy && order.order) {
query.orderBy = order.orderBy;
query.order = order.order;
}
query.page = page+1;
query.perPage = perPage;
setLoading(true);
try {
await get(query);
}
catch(error) {
console.log(error);
props.onError(error);
}
setLoading(false);
}
_getUsers();
}, [page, perPage, order, type, debouncedSearch]);
}
On initial render, debounce effect will setup a debounce timer... but it is okay.
After debounce delay, it will set deboucedSearch state to same value.
As deboucedSearch has not changed, ferch effect will not run, so no wasted fetch.
Subsequently, on change of any query param except search, fetch effect will run immediately.
On change of search param, fetch effect will run after debouncing.
Ideally though, debouncing should be done at <input /> of search param.
Small issue with doing debouncing in fetching component is that every change in search will go through debouncing, even if it is happening through means other than typing in text box, say e.g. clicking on links of pre-configured searches.
The rule around hook dependencies is pretty simple and straight forward: if the hook function use or refer to any variables from the scope of the component, you should consider to add it into the dependency list (https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies).
With your code, there are couple of things you should be aware of:
1.With the first _getUsers useEffect:
useEffect(() => {
_getUsers()
}, [page, perPage, order, type])
// Correctly it should be:
useEffect(() => {
_getUsers()
}, [_getUsers])
Also, your _getUsers function is currently recreated every single time the component is rerendered, you can consider to use React.useCallback to memoize it.
2.The second useEffect
useEffect(() => {
if(firstUpdate.current)
firstUpdate.current = false;
else
_debounceSearch()
}, [search])
// Correctly it should be
useEffect(() => {
if(firstUpdate.current)
firstUpdate.current = false;
else
_debounceSearch()
}, [firstUpdate, _debounceSearch])