Mobx: change states in action without runInAction still works - reactjs

In mobx documentation:
action only affects the currently running function, not functions that are scheduled (but not invoked) by the current function! This means that if you have a setTimeout, promise.then or async construction, and in that callback some more state is changed, those callbacks should be wrapped in action as well!
This above mean, I should wrap state changed with runInAction, like this following:
class App {
#observable logined = false
#action async login(payload){
runInAction(() => {
setTimeout(() => {
this.logined = false
}, 1000)
})
}
}
Above works, but the weird is if i remove the runInAction block, code still works, this behaviour is inconsistent with the document said.
please check the fiddle.

This behavior is correct; unobserved data can be modified at will, as it cannot lead to further side effects, see: https://github.com/mobxjs/mobx/blob/master/CHANGELOG.md#310
Also not that it is always allowed to change state outside of actions as long as strict mode is not enabled (mobx.useStrict(true))
A PR to reflect this new behavior better in the docs would be appreciated! https://github.com/mobxjs/mobx/blob/gh-pages/docs/refguide/action.md

An "action" allows you to make multiple state changes in one "batch" -- if you are only making one change, you don't need it.

Related

Can anyone give more info about the useEffect hook based on experience

I understand a bit about the useEffect hook but I think there’s still more knowledge to grasp. Some of which are not in the documentation. Please any contribution will help a lot y’all.
Some of my questions are:
Does useEffect get called on initial render even in production just like in development?
If it does, how do we control this the best way?
How can we use a clean up function on this Hook?
How can we make asynchronous calls in useEffect?
My attempts on useEffect usually makes me feel like a bad developer
Please take a look at react docs and react beta docs.
It always runs when your component mounts, after the first render regardless of the environment. In development mode when strict mode is on, it runs twice:
When Strict Mode is on, React will run one extra development-only setup+cleanup cycle before the first real setup. This is a stress-test that ensures that your cleanup logic “mirrors” your setup logic and that it stops or undoes whatever the setup is doing. If this causes a problem, you need to implement the cleanup function.
I'm not really sure what you mean by controlling it the best way. Your effect or setup code runs whenever the component mounts. Maybe
How to handle the Effect firing twice in development? can help you. You sometimes might want to prevent the effect to be executed when the component mounts, you can skip the effect by using a ref. See this stackoverflow question
The function you return in the useEffect does the clean up for you. See. For instance if you add an event listener inside useEffect, you remove the listener inside the function you return inside of it. See this link
useEffect(() => {
const listener = () => { /* Do something */ };
window.addEventListener("click", listener);
return () => {
window.removeEventListener("click", listener);
};
}, []);
Yes you can. See this stackoverflow question and fetching data in docs
useEffect(() => {
async function asyncFunction() {
/* Do something */
}
asyncFunction();
}, []);
Update:
Take a look at You Might Not Need an Effect
. It explains some situations which you might not need an effect at all.
Removing unnecessary Effects will make your code easier to follow, faster to run, and less error-prone.
Update 2:
You can probably skip this part for now, but it might help you to have a better grasp of useEffect, event handlers and what to expect in the future.
Separating Events from Effects tries to explain the differences between effects and event handlers, why distinguishing between those two is important and using event handlers inside effects.
Event handlers only re-run when you perform the same interaction again. Unlike event handlers, Effects re-synchronize if some value they read, like a prop or a state variable, is different from what it was during the last render. Sometimes, you also want a mix of both behaviors: an Effect that re-runs in response to some values but not others. This page will teach you how to do that.
Sometimes, you might use an event handler which has access to the props or the state inside an effect. But you don't want the useEffect to be triggered every time the values used in the event handler change. The following example is taken form useEffect shouldn’t re-fire when event handlers change
.
function Chat({ selectedRoom }) {
const [muted, setMuted] = useState(false);
const theme = useContext(ThemeContext);
useEffect(() => {
const socket = createSocket('/chat/' + selectedRoom);
socket.on('connected', async () => {
await checkConnection(selectedRoom);
showToast(theme, 'Connected to ' + selectedRoom);
});
socket.on('message', (message) => {
showToast(theme, 'New message: ' + message);
if (!muted) {
playSound();
}
});
socket.connect();
return () => socket.dispose();
}, [selectedRoom, theme, muted]); // 🟡 Re-runs when any of them change
// ...
}
As you see, you do not want to reconnect every time theme or muted variables change. The only time you want the effect(connecting and disconnecting from the server) to run is when the selectedRoom value changes.
So the react team has proposed a RFC: useEvent which provides
A Hook to define an event handler with an always-stable function identity.
useEvent is an experimental and unstable API that has not yet been added to the React(stable versions) ye, so you can’t use it yet.
This might be off-topic but probably helps you to understand React and its lifecycles better: There is this issue useCallback() invalidates too often in practice issue on GitHub . One workaround would be to create a custom hook that returns a function that its identity is stable and won't change on re-renders:
function useEventCallback(fn) {
let ref = useRef();
useLayoutEffect(() => {
ref.current = fn;
});
return useCallback(() => (0, ref.current)(), []);
}
Or you could use the use-event-callback package.
Note that useEventCallback does not mimic useEvent precisely:
A high-fidelty polyfill for useEvent is not possible because there is no lifecycle or Hook in React that we can use to switch .current at the right timing. Although use-event-callback is “close enough” for many cases, it doesn't throw during rendering, and the timing isn’t quite right. We don’t recommend to broadly adopt this pattern until there is a version of React that includes a built-in useEvent implementation.
useEffect is a very powerful hook. Regarding your question:
useEffect(() => (), []) - this version without params will be called once on initial rendering
you can control useEffect with params [] and based on these params you can place some logic inside the callback function.
clean up function used before unmount of your component, it is a good place to remove listeners or close connection to resources like Databases, Camera and etc.
Example of async call
useEffect(() => {
// declare the data fetching function
const fetchData = async () => {
const data = await fetch('https://yourapi.com');
}
// call the function
fetchData()
// make sure to catch any error
.catch(console.error);
}, [])

UseEffect, Re-run on state change?

useEffect(() => {
if (params.usersearch === props.search || "random" === props.search) {
console.log('same');
return;
}else{
console.log(props.search);
console.log('Params is :',params.usersearch);
let api = `https://www.flickr.com/services/rest/?method=flickr.photos.search&api_key=${apiKey}&tags=${params.usersearch}&per_page=24&format=json&nojsoncallback=1`;
props.handleLoading(true); //changing state
let fetchedData = axios
.get(api)
.then((data) => data.data.photos.photo)
.catch((err) => console.log(err));
props.handleData(fetchedData); //changing state
props.handleSearch(params.usersearch); //changing state
props.handleLoading(false); //changing state
const title = document.querySelector("title");
title.textContent = `Flickr-Photos/${params.usersearch}`;
}
},[params.usersearch]);
Hi everyone. I have a question , If my useEffect is running and it in between changes state(as I have mentioned when the props.handleLoading function gets triggered ), so is it gonna stop the and re-run the useEffect method or it is gonna complete the code?
This kind of code execution cannot be implicit stopped.
Answer:
The useEffect-callback will be called again, even if the previous is not done
You could use a debounce or blocking behavior and cancel/ignore the previous action.
UseEffect supports a clean-up method. You could return a function to cancel a timer with a given throttle value (for debounce).
As stated by the react docs
Why did we return a function from our effect? This is the optional cleanup mechanism for effects. Every effect may return a function that cleans up after it. This lets us keep the logic for adding and removing subscriptions close to each other. They’re part of the same effect!
When exactly does React clean up an effect? React performs the cleanup when the component unmounts. However, as we learned earlier, effects run for every render and not just once. This is why React also cleans up effects from the previous render before running the effects next time. We’ll discuss why this helps avoid bugs and how to opt out of this behavior in case it creates performance issues later below.
Solution
Stackblitz Example

RxJS and repeated events

I am new to RxJs in general but am investigating a bug in some React code in which, upon an unrelated action, an old event seems to be emitted and rendered to a display error. Think if you had two buttons that generated two messages somewhere on screen, and clicking one button was showing the message for the other button.
Being new to RxJs I'm not positive where the problem lays. I don't see a single ReplaySubject in the code, only Obserables, Subjects, and BehaviourSubjects. So this is either misuse of an RxJs feature or just some bad logic somewhere.
Anyway I found the code with the related Observable and I'm not quite sure what this person was trying to accomplish here. I have read up on combineLatest, map, and pipe, but this looks like pointless code to me. Could it also be somehow re-emitting old events? I don't see dynamic subscriptions anywhere, especially in this case.
Tldr I don't understand the intent of this code.
export interface IFeedback {
id: number
text: string
}
export interface IFeedbackMessages {
message: IFeedback | undefined
}
feedback$ = new BehaviorSubject<IFeedback | undefined>(undefined)
feedbackNotifs$: Observable<IFeedbackMessages> = combineLatest([
feedback$
]).pipe(
map(([feedback]) => ({
feedback
})
))
I also found this which maybe be an issue. In the React component that displays this message, am I wrong but does it look like each time this thing renders it subscribes and then unsubscribes to the above Subject?
const FeedbackDisplay: React.FC () => {
const [feedbackNotifications, setFeedbackNotifications] = React.useState<IFeedbackMessages>()
React.useEffect(() =>
{
const sub = notification$.subscribe(setFeedbackNotifications)
return () => sub?.unsubscribe()
}, [notifications$])
}
Could it also be somehow re-emitting old events?
Yes, it probably is. BehaviorSubject has the unique property of immediately emitting the last value pushed to it as soon as you subscribe to it.
It's great when you want to model some persistent state value, and it's not good for events whose actual moment of occurrence is key. It sounds like the feedback messages you're working with fall into the second category, in which case Subject is probably a better choice.
does it look like each time this thing renders it subscribes and then unsubscribes to the above Subject?
Not exactly. useEffect accepts a callback, and within that callback you can optionally return a "cleanup" function. React will hang onto that function until the effect is triggered again, then it calls it to clean things up (which in this case consists of closing out the subscription) to make room for the next effect.
So in this case, the unsubscribe will only happen when the component is rendered with a new value for notifications$. Also worth pointing out that notifications$ will only change if it's either passed as a prop or created within the component function. If it's defined outside the function (imported from another file for example), you don't need to (and in fact should not) put it into useEffect's dependency array.

How can I make a function wait for another function?

I have this function:
const submitImport = async (value) => {
const { actions, navigation, account } = this.props;
this.setState({ isImporting: true });
actions.importWallet(value.mnemonicPhrase, value.password, 1);
console.log('acc', account);
actions.showNotification({
message: 'Account has been successfully Imported',
isError: false,
});
};
importWallet is adding new properties to account object but when I call this function the first time the account object is empty but when I click it the second time it is okay. So my guess is importWallet needs time to finish and return the value. I tried to use async await but it did not work. Also, I tried to use return new Promise but it still did not work. Maybe I did it the wrong way idk.
Any suggestions on how to solve this issue please?
I am assuming the importWallet function induces some change in the account prop.
All prop changes require atleast one render cycle for the updated values to get visible as state/prop changes are asynchronous in react. In your case you are trying to access account as soon invoking actions.importWallet. Hence as it is within the same render, it has not yet been updated. But as you mentioned, it will be available from the subsequent renders.
Also you cannot make it synchronous with async-await / promises as the asynchronous nature of react state updates is not exposed.
Your usecase may be achieved by some refractor :
Try obtaining the new value of account in the return statement of account.importWallet. That way you can use it even before the prop updates.
const updatedAccount = actions.importWallet(value.mnemonicPhrase, value.password, 1);
In case you are using React Hooks, you can create an effect with useEffect and add account as a dependency. This will invoke the effect when the dependency value changes (dependency should be a primitive data type for consistent effect invocation).

useState React hook always returning initial value

locationHistory is always an empty array in the following code:
export function LocationHistoryProvider({ history, children }) {
const [locationHistory, setLocationHistory] = useState([])
useEffect(() => history.listen((location, action) => {
console.log('old state:', locationHistory)
const newLocationHistory = locationHistory ? [...locationHistory, location.pathname] : [location.pathname]
setLocationHistory(newLocationHistory)
}), [history])
return <LocationHistoryContext.Provider value={locationHistory}>{children}</LocationHistoryContext.Provider>
}
console.log always logs []. I have tried doing exactly the same thing in a regular react class and it works fine, which leads me to think I am using hooks wrong.
Any advice would be much appreciated.
UPDATE: Removing the second argument to useEffect ([history]) fixes it. But why? The intention is that this effect will not need to be rerun on every rerender. Becuase it shouldn't need to be. I thought that was the way effects worked.
Adding an empty array also breaks it. It seems [locationHistory] must be added as the 2nd argument to useEffect which stops it from breaking (or no 2nd argument at all). But I am confused why this stops it from breaking? history.listen should run any time the location changes. Why does useEffect need to run again every time locationHistory changes, in order to avoid the aforementioned problem?
P.S. Play around with it here: https://codesandbox.io/s/react-router-ur4d3?fontsize=14 (thanks to lissitz for doing most the leg work there)
You're setting up a listener for the history object, right?
Assuming your history object will remain the same (the very same object reference) across multiple render, this is want you should do:
Set up the listener, after 1st render (i.e: after mounting)
Remove the listener, after unmount
For this you could do it like this:
useEffect(()=>{
history.listen(()=>{//DO WHATEVER});
return () => history.unsubscribe(); // PSEUDO CODE. YOU CAN RETURN A FUNCTION TO CANCEL YOUR LISTENER
},[]); // THIS EMPTY ARRAY MAKES SURE YOUR EFFECT WILL ONLY RUN AFTER 1ST RENDER
But if your history object will change on every render, you'll need to:
cancel the last listener (from the previous render) and
set up a new listener every time your history object changes.
useEffect(()=>{
history.listen(()=>{//DO SOMETHING});
return () => history.unsubscribe(); // PSEUDO CODE. IN THIS CASE, YOU SHOULD RETURN A FUNCTION TO CANCEL YOUR LISTENER
},[history]); // THIS ARRAY MAKES SURE YOUR EFFECT WILL RUN AFTER EVERY RENDER WITH A DIFFERENT `history` OBJECT
NOTE: setState functions are guaranteed to be the same instance across every render. So they don't need to be in the dependency array.
But if you want to access the current state inside of your useEffect. You shouldn't use it directly like you did with the locationHistory (you can, but if you do, you'll need to add it to the dependency array and your effect will run every time it changes). To avoid accessing it directly and adding it to the dependency array, you can do it like this, by using the functional form of the setState method.
setLocationHistory((prevState) => {
if (prevState.length > 0) {
// DO WHATEVER
}
return SOMETHING; // I.E.: SOMETHING WILL BE YOUR NEW STATE
});

Resources