React Hooks: Referencing data that is stored inside context from inside useEffect() - reactjs

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!

Related

How to properly re-render functional component after API call

When the page loads, I am making an API call, displaying a table with appointments. After the API call, I set a state for hasData to true, and the data is inserted in another setState. The issue is when the API returns the data from the async call, the component does not show the data. Please see code below.
const [recentAppointmentData, setRecentAppointmentData] = useState([])
const [hasAppointmentData, setHasAppointmentData] = useState(false)
const getAppointments = useCallback(() => {
const getAppointmentDataService = new GetAppointmentsService();
getAppointmentDataService.getDataFromService("263749804").then((results) => {
console.log("APPOINTMENT DATA ", results);
results.recentAppointments.map((result) => {
var recentAppointments = {
appointmentObject: {
serviceCategory: [],
serviceId: "",
appointmentDate: "",
groomer: "",
resourceId: "",
visitId: "",
},
};
if (result["services"] !== undefined) {
console.log("SERVICESS", result["services"]);
result["services"].map((service) => {
recentAppointments.appointmentObject.serviceCategory.push(
service["serviceCategory"]
);
recentAppointments.appointmentObject.serviceId = service["serviceId"];
});
}
recentAppointments.appointmentObject.appointmentDate = moment(
result["appointmentDateTime"]
).format("MM/DD/YY");
recentAppointments.appointmentObject.groomer = result["groomer"];
recentAppointments.appointmentObject.resourceId = result["resourceId"];
recentAppointments.appointmentObject.visitId = result["visitId"];
appointments.push(recentAppointments.appointmentObject);
Here I am setting the has Appointment data to true after the async function has been completed.
if (!hasAppointmentData) {
setHasAppointmentData(true);
}
});
Here I am storing the data in another state.
if (!hasAppointmentData) {
console.log("APPOINTMEN", appointments);
setRecentAppointmentData(appointments);
}
});
}, [hasAppointmentData]);
I am calling the function in the useEffect.
useEffect(() => {
getAppointments();
renderTabs();
}, [getAppointments, renderTabs]);
Can someone guide me on what I am doing wrong? Thanks
The problem is that you're using the useEffect hook wrong.
useEffect runs every time one of its dependencies change, or runs just once when the component mounts if you don't pass in any dependency to it. The dependencies are usually state variables within the component that useEffect runs in.
You want your getAppointments() to run only once, since it calls an external API to get the data. And you want to call renderTabs() (which I assume is responsible for displaying the data in the UI) only when the data is available. So you need to put them into two separate useEffect hooks.
useEffect(() => {
getAppointments();
}, []); // Runs just once when the component is mounted
useEffect(() => {
if (hasAppointmentData) {
renderTabs();
}
}, [hasAppointmentData]); // Runs every time the value of hasAppointmentData changes
But you'll need to watch out for a problem here, when using hasAppointmentData as the dependency. You're calling setHasAppointmentData first, and then following it up with setRecentAppointmentData. The second useEffect hook would run right after you set the boolean to true. By the time renderTabs() tries to fetch the data from recentAppointmentData, the data may not have been updated.
To me, hasAppointmentData is pretty much useless here. Checking for recentAppointmentData.length would serve you just as well, and is guaranteed to work reliably every time. So my second hook would look like this:
useEffect(() => {
if (recentAppointmentData.length) {
renderTabs();
}
}, [recentAppointmentData.length]);

How to re-render a component when a non state object is updated

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.

Using useEffect properly when making reqs to a server

I have a handleRating function which sets some state as so:
const handleRating = (value) => {
setCompanyClone({
...companyClone,
prevRating: [...companyClone.prevRating, { user, rating: value }]
});
setTimeout(() => {
handleClickOpen();
}, 600);
};
I think also have a function which patches a server with the new companyClone values as such:
const updateServer = async () => {
const res = await axios.put(
`http://localhost:3000/companies/${companyClone.id}`,
companyClone
);
console.log("RES", res.data);
};
my updateServer function gets called in a useEffect. But I only want the function to run after the state has been updated. I am seeing my res.data console.log when I load my page. Which i dont want to be making reqs to my server until the comapanyClone.prevRating array updates.
my useEffect :
useEffect(() => {
updateServer();
}, [companyClone.prevRating]);
how can I not run this function on pageload. but only when companyClone.prevRating updates?
For preventing function call on first render, you can use useRef hook, which persists data through rerender.
Note: useEffect does not provide the leverage to check the current updated data with the previous data like didComponentMount do, so used this way
Here is the code example.
https://codesandbox.io/s/strange-matan-k5i3c?file=/src/App.js

React useEffect infinite loop fetching data from an api

Hi I'm trying to make a twitter clone app. I am using React on the client side and Express on the server side and PostgreSQL as my database. So here's the problem, I'm trying to use the useEffect like this:
const [tweets, setTweets] = useState([]);
const getTweets = async () => {
const res = await api.get("/posts", {
headers: { token: localStorage.token },
});
setTweets(res.data);
};
useEffect(() => {
getTweets();
}, [tweets]);
I have no idea why it's looping infinite times, am I using it correctly though? I want the tweets to be updated every time I post a tweet. It's working fine but it's running infinite times. I just want it to re-render if a tweet got posted.
Here's my server code for getting all the posts:
async all(request: Request, response: Response, next: NextFunction) {
return this.postRepository.find({
relations: ["user"],
order: {
createdAt: "DESC",
},
});
}
The problem is every time you change the tweets it executes useEffect and changes the tweets and so long and so forth, so it's natural that it loops infinitely, the solution is to add a trigger that you set to true when a tweet gets posted, so the solution would be like this
const [tweets, setTweets] = useState([]);
const [isFetching, setIsFetching] = useState(false);
const getTweets = async () => {
const res = await api.get("/posts", {
headers: { token: localStorage.token },
});
setTweets(res.data);
};
useEffect(() => {
getTweets();
setIsFetching(false);
}, [isFetching]);
and set some logic to use setIsFetching(true) in order to execute the useEffect
PS: if you use an empty array in useEffect, it would execute only when the component is mounted (at the start)
useEffect(() => {
getTweets();
}, [tweets]); // [tweets means that hook works every time 'tweets' state changes]
so your getTweets function set tweets => as tweets are changed hook works again => call getTweets => ... = infinite loop
if you want to download tweets, use empty array instead - hook will work once then
Pass empty array as a second arg for calling it once otherwise for changing it on every tweet change it will re-trigger, so whenever state will change only then it will be re-rendered like Tarukami explained. One thing you can do is check the length like mentioned below so not to compare the whole object but just the length
useEffect(() => {
getTweets();
}, [tweets.length]);
This might raise an error react-hooks/exhaustive-deps lint error (that's a bypass you can use it).
But if you want more tighter check you can compare the ids on each re-render (create a hash/key/id from all element in the array and compare them on each render) like so [tweet id here]) // Only re-subscribe if id changes

How do I trigger a call to dispatch using React Hooks?

I have a save function in a component that makes some calls to an API and changes my local state based on the response. I then want to dispatch an action once AND ONLY ONCE these calls to setState are complete e.g.
const [myData, setMyData] = useState([{id: 0, name: "Alex"}]);
const save = async () => {
if (someCondition) {
let response = await axios.get("/someroute");
if (response.status === 200) {
setMyData([...myData, response.data])])
}
}
if (someOtherCondition) {
let response2 = await axios.get("/someotherroute");
if (response2.status === 200) {
setMyData([...myData, response2.data])])
}
}
dispatch(myAction(myData));
}
Now I know that useEffect should be the way to go about this. e.g.
useEffect(() => {
dispatch(myAction(myData));
}, [myData]);
however i'm changing myData at other places in the component, and I only want to dispatch the action once save() has been called and the setMyData calls have finished. I can't think of a condition to check for in useEffect() except maybe adding some kind of flag to myData, like an object saying "Ready to save", but it feels like there should be a simpler way.
Massively appreciate any help on this one!
What do you mean by changing myData at other places? Are you changing myData by fetching it from somewhere? Because if you don't, setMyData will do its job pretty straightforward and fast. So your save function won't need to listen it.
If you change myData as a result of some other fetching and save function should wait it. Then story can little bit complicated. You might like to check middlewares and redux sagas.
I have hard time to understand your scenario and use case; but if state overwriting is your concern during consecutive fetch actions then saga middleware can take care of it.
For example you can create some saga middleware with takeLatest so that it will take last action that dispatched and kill the previous one (not waiting) that ongoing; so you would avoid data overwrite that might occur due side effect. Or similarly you might want to use takeEvery, which will queue every action for setting myData and they will wait each other to end.
check more : https://redux-saga.js.org/
and usage example : https://codesandbox.io/s/385lk3x6v1?file=/src/sagas/index.js
It's looks like you need to use useReduce instead and then you are able to detect when any of the props change with the useEffect:
const initialState = {
id: null,
name: null,
...
}
const [state, dispatch] = useReducer((state, action) => {
switch (action.type) {
case 'CHANGE-NAME':
return { ...state, name: action.value }
default:
throw new Error('Type not defined')
}
}, initialState)
useEffect(() => {
// Notify changes!!
}, [state.name]);
...
const myFun = async () {
// do something
dispatch({ type: 'CHANGE-NAME', value: 'Hello world' })
}
Also you can add other props to your initial state, for example if you need a specific flag to detect an action once :)
Regards

Resources