I always run into situations where I need to force rerender, while I'm still in the execution of some function, so I developed my solution to this and I need to know if this is right or there is a simpler way to achieve the same goal.
I rely on the state variable my_force_update, then I change it to a random value when I want to enforce a change. like:
const [my_force_update, setMyForceUpdate] = useState(0);
useEffect(()=>{}, [my_force_update]);
const handleSubmit = async () =>{
await prm1();
stMyForceUpdate(Math.random()); // enforcing the effect
await prom2();
....
}
so I have been able to enforce re-render (by enforcing the effect) while I'm still in the handleSubmit execution.
is there a simpler way? or, did I mistakenly understand the concepts of React?
update
The issue is that I have a checkout form, and I need it to be a signup form at the same time, and there is also a login component on the page.
so I need to populate the form fields with the account if information in case of login and in case of sign up.
The steps are as follow:
if user login => populate form (per fill it with user info) => move to payment.
if user fill out the form manually:
create an account.
authenticate the new user.
update the user account.
repopulate form (with data from user account).
move to payment.
so I have this function that needs to listen to the login and signup:
const token = useSelector(_token);
const loggedIn = useSelector(_loggedIn);
const profile = useSelector(_profile);
useEffect(() => {
/**
* Pre-fill the form inputs
*/
(async () => {
const r = await dispatch(fetchUserInfo());
setFormProfile(profile); // address is not updated yet
setFormAddress(r?.result?.address);
})();
}, [loggedIn, forceUpdate]);
now, there are no issues with the login process, the only problem is with the signup:
at step 2, when authenticating the user, its account is empty.
so the loggedIn changes to true when the profile is empty so I got empty form.
after updating the profile, loggedIn will not change, so I need another variable to trigger the effect again.
I tried to listen to profile here, but I got an infinite loop.
and here is the checkout flow related to the signup:
...
if (!loggedIn) {
const signupResponse = await dispatch(signupUser(params));
loginResponse = await dispatch(login(formProfile?.email, password));
}
const updateProfileResponse = await saveChangesToProfile();
// update user profile with the information in the checkout form.
...
then save changes to the profile:
const saveChangesToProfile = async () => {
const r = await dispatch(fetchUserInfo());
const addressID = r?.result?.address_id;
const res1 = await dispatch(updateUserAddress(addressID, { ID: addressID, ...formAddress }));
const res = await dispatch(UpdateUser(r?.result?.ID, formProfile));
setForceUpdate(Math.random()); // force re-render to re-populate the form.
setSuccess("Information saved to your profile!");
return res;
};
Update 2
The question is general, I solved the issue in another way days ago (involving changes to the server routes). and I'm asking the question in a general way to get some knowledge, not for others to do the work for me.
In general, you should avoid having to force an update in React but instead use existing React features to accomplish your goal. That being said, there are simple ways to force a re-render in react. You mentioned in the second update that you are looking for more general solutions - so I will provide them here.
However, please bear in mind that this topic has been discussed extensively in other stack overflow questions (I will provide links).
Forcing Re-Render using component.forceUpdate(callback)
The react docs actually list a simple way to force a component to reload (provided you maintain a reference to it). You can find more information here, but essentially it forces your component to re-render and then makes a call to the callback argument.
Forcing Re-Render using hooks
There are multiple stack overflow questions that provide simple code snipets that can force a react component to re-render by using hooks. This answer for example by #Qwerty demonstrates 2 simple code snipets to force a re-render:
const forceUpdate = React.useState()[1].bind(null, {}) // see NOTE above
const forceUpdate = React.useReducer(() => ({}))[1]
You should check out his answer for a more detailed explanation.
Other sources include this answer to the same stack overflow question that references the official FAQ.
It solves the problem by doing:
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);
Solving Your Specific Problem
I saw that you were able to solve your problem by using the useEffect hook - a great start for a potential solution. You also mentioned that you got an infinite loop while listening to a variable change in your hook - a common problem and one with some common solutions. In general, you should always run a check inside the useEffect hook before changing any of its dependencies. For example, run a check to see if the profile is unset before trying to update its value.
I however would recomend that you use a progress varible that would indicate your status, something like this:
const STATUS_START = 0;
const STATUS_LOGED_IN = 1;
const STATUS_SIGNING_UP = 2;
const [progress, setProgress] = useState(STATUS_START);
Then, you can simply listen to changes made to the progress variable in your useEffect hook (by passing it as your only dependent). This should automatically condition you to write the necessary logic to check for state inside of the useEffect function as I described previously.
This solution would work by initially setting the progress to either signing up or logging in, but only filling the form data if you are logged in (and after the signup progress is done calling setProgress(STATUS_LOGED_IN))
Related
I am working with ReactJS and Google Firestore. I have a component called GameEntryForm, where you can select from a list of users stored in Firestore. In order to get this list, when I render the GameEntryForm component, I make a query to Firestore. Below is how I am getting the list.
I was wondering if there was a better or faster way to do this. My concern is that as the number of users increases, this could be a slow operation.
function GameEntryForm() {
// prevent rendering twice
const effectRan = useRef(false);
const [usersList, setUsersList] = useState(new Map());
useEffect(() => {
if (effectRan.current === false) {
const getUsers = async () => {
const q = query(collection(firestore, "users"));
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
setUsersList(new Map(usersList.set(doc.data().uid, doc.data())));
});
};
getUsers();
return () => {
effectRan.current = true;
};
}
}, []);
}
Your code looks fine at first glance, but
here are many ways to mitigate this issue some of them are as follows:
Implement Pagination Functionality to limit the number of documents that are returned by the query, for more about this topic go through this docs
Use Firestore Offline Caching feature through persistence like one provided here. I understand that your user will be added constantly so there’s not much improvement with this method but you can trigger a new request to the db based on the changed type. This is nicely explained in this thread
You can also use the above caching with a global state management solution(Redux, Context API) and only fetch the list of users once. This way, the list of users would be accessible to all components that need it, and you would only have to make the query once. Someone has created an example for how this will work although not using firestore though.
Last but not least use Real Time lister to View changes between snapshots as provide here in official docs This works great with the offline Caching option.
I am creating an easy chat app, with different text channels. I am facing an infinite loop issue when using the useEffect hook to update the messagesList on real time. You can see the code below but this is what I am trying to achieve:
First useEffect is for the chat window to scroll to the last message every time there is a change in the messagesList array. This means: I am in the middle of the messages window, I write a new message and it takes me to the bottom. This is working fine.
Second useEffect is for the messagesList to be rendered whenever the channel is changed or there is any change in the messagesList. Adding the messagesList as a dependency is causing the infinite loop... but I think I need it cause otherwise the following happens: user1 is inside the chat channel and user2 writes a new message. User1 wont see the new message displayed as his chat is not being re-rendered. How would you make it for the new message to be displayed for user1?
Sorry for the confusing question and thanks a lot in advance!
useEffect(() => {
anchor.current.scrollIntoView(false);
}, [messagesList]);
useEffect(() => {
const unsubscribe = onSnapshot(
collection(firestore, `channels/${activChannel}/messages`),
(snapshot) => {
console.log(snapshot.docs);
getMessagesList();
}
);
return () => unsubscribe();
}, [activChannel, messagesList]);
I am not familiar with firestore, but perhaps you could tie the updating of the messages to the event that an user submits his message or use useSyncExternalStore. This piece of documentation on useEffect use cases might help you.
an excerpt from the docs:
Here, the component subscribes to an external data store (in this
case, the browser navigator.onLine API). Since this API does not exist
on the server (so it can’t be used to generate the initial HTML),
initially the state is set to true. Whenever the value of that data
store changes in the browser, the component updates its state.
Although it’s common to use Effects for this, React has a
purpose-built Hook for subscribing to an external store that is
preferred instead. Delete the Effect and replace it with a call to
useSyncExternalStore:
This is for an open-source project called react-share, and their ShareButton component has a prop called beforeOnClick that you can pass to it. I'm using beforeOnClick to upload an image to our CDN so that we don't needlessly upload images that don't get shared, which causes the url prop passed to the button to update.
My current problem is, after beforeOnClick runs, the share button currently doesn't handle the updated url prop.
Basically, I have an async function that looks something like this:
const handleClick = async () => {
const { url, disabled, beforeOnClick } = this.props;
// beforeOnClick can cause this.props to change. beforeOnClick can also perform async operations, like making a fetch call
if (beforeOnClick) {
await beforeOnClick();
// call setTimeout to delay the next handleClick call in order to ensure this.props
// properly reflects changes from the parent component
setTimeout(handleClick);
return;
}
// Do stuff with url & disabled
};
I dumbed it down for the sake of keeping the question simple, but if you'd like to view the code I currently have, check out my fork. compare to the original.
Is setTimeout a reliable way to achieve this effect? Or, should I do something like this instead:
this.setState({ __rerender_component: true }, handleClick);
I'm not a huge fan of that solution, as I'd have to manage resetting that flag after the callback is run. Any thoughts are appreciated!
EDIT: Using setTimeout seems to work, but I'm not sure if it's reliable. If it fails 1/100 times, that sucks.
It might be easiest and feels more "reacty" to use setState to have a local copy of the props and let the beforeOnClick function use setState?
eg (beware, I have been using hooks only on my latest projects, so might be off)
const handleClick = async () => {
this.state = this.props; // can all props be changed?
if (beforeOnClick) {
await beforeOnClick(this.setState);
// Do stuff with this.state.url & this.state.disabled };
and beforeOnClick can use setState to change the url and others.
instead of giving full control to setState, you might want to have a different approach:
let newState= await beforeOnClick();
if (newState && newState.url && !newState.url.startsWith("http"))
throw 'url must start with http';
// that might be a wrong assumption, take it as an example
// whatever else you want to check, like disable is a boolean...
this.setState({...state, ...newState});
i'm new to react please forgive me if i'm asking a dumb question.
The idea is to access the tweets array from context, find the matching tweet and then set it in the component's state to access the data.
However, the tweets array results empty even though i'm sure it's populated with tweets
const { tweets } = useContext(TweeetterContext)
const [tweet, setTweet] = useState({})
useEffect(() => {
loadData(match.params.id, tweets)
}, [])
const loadData = (id, tweets) => {
return tweets.filter(tweet => tweet.id == id)
}
return (stuff)
}
You are accessing context perfectly fine, and it would be good if you could share a code where you set tweets.
Independent of that, potential problem I might spot here is related to the useEffect function. You are using variables from external context (match.params.id and tweets), but you are not setting them as dependencies. Because of that your useEffect would be run only once at the initial creation of component.
The actual problem might be that tweets are set after this initial creation (there is some delay for setting correct value to the tweets, for example because of the network request).
Try using it like this, and see if it fixes the issue:
useEffect(() => {
loadData(match.params.id, tweets)
}, [match.params.id, tweets])
Also, not sure what your useEffect is actually doing, as it's not assigning the result anywhere, but I'm going to assume it's just removed for code snippet clarity.
In my mind, whenever I call setState of React Component I believe at least a little work is done to refresh the Component. To store every important dynamic variable in the state is a niche I have seen so far in the community.
Imagine this none trivial code that sets searching variable to true when a server request is yet to return.
fetchSearch = async (food) => {
this.searching = true;
const request = await client.get(`http://localhost:8081/food?q=${food}`);
const results = await request.json();
this.setState((state) => {
this.searching = false;
return { searchResult: results }
});
}
<Input showSpinner={this.searching}</Input>
As you may have guessed the value of this.searching is a determinant of if the Loading UI animation CSS class is applied.
My question is if it is really worth setting this.searching in the state with the extra WORK of refreshing the Component, as opposed to using an instance value ? What are the possible troubles I can run into ? What benefit do I derive other than having a central store for instance ?
Since you are already updating the state for searchResult, updating another field searching will not cause additional rerenders.
If your UI relies on searching you will need to have it in the state, otherwise react would not know that searching value has changed.
React is already very performant, there is no need to overthink before you actually encounter any issues with the performance.