Why mobx needs actions, why not just to batch all mutations into setImmediate? - mobx

I am starting to learn mobx, and I do not understand why mobx invented "actions" entity. Will it be easier just to batch all changes into next tick inside setImmediate? That will automatically make all sync state changes act in the same way as #action do now. Is there any profit of triggering observers right just after action finished instead of inside next tick?

According to conversation from https://github.com/mobxjs/mobx/issues/1523 :
setImmediate implemented only in IE and Nodejs. setTimeout(fn, 0) can cause 4 ms delay
https://hackernoon.com/the-fundamental-principles-behind-mobx-7a725f71f3e8 : async update will not have such good stack trace to find who was triggering that update
React batches only setState calls from event listeners, and so do mobx-react couple. It means that React component will not re-render immediately after mutation, it will synchronously update only after event listener callback.
Few examples (all jsx component are supposed to be inside #observer):
// Ok to do this
<input value={appState.name} onChange={e => appState.name = e.target.value}/>
// Ok too, will rerender only once
<input value={appState.name} onChange={e => {
appState.name = e.target.value;
appState.foo = appState.foo + 1;
}}/>
// Not very good, will rerender twice on each click.
// Nevertheless, I do not know why someone will do this
<button onClick={() => {
setTimeout(() => {
appState.foo = appState.foo + 1;
appState.bar = appState.bar + 10;
}, 0);
}}>Click me foo={appState.foo} bar={appState.bar}</button>
// But that way it will rerender once and show console output only once too
autorun(() => console.info(`App state foo={appState.foo} bar={appState.bar}`));
<button onClick={() => {
setTimeout(() => {
runInAction(() => {
appState.bar = appState.bar + 10;
appState.foo = appState.foo + 1;
})
}, 0);
}}>Click me foo={appState.foo} bar={appState.bar}</button>
// That code will show console output twice, but rerender only once
autorun(() => console.info(`App state foo={appState.foo} bar={appState.bar}`));
<button onClick={() => {
setTimeout(() => {
ReactDOM.unstable_batchedUpdates(() => {
appState.bar = appState.bar + 10;
appState.foo = appState.foo + 1;
})
}, 0);
}}>Click me foo={appState.foo} bar={appState.bar}</button>

Related

UI not rendering while useEffect fires

I'm trying to get dynamic next page with Object.entries[currentTaskIndex], everything works great and i'm getting the task data based on their indexes , but useEffects not rendering UI when dependency changes, actually it's rendering same task as before ,so this mean when useState fire, currentTaskIndex state changes to 2 but it's stillshowing index 1 items, but when i refresh the page it's showing index 2 items,so the question is that, how can i show index 2 items while useEffects fire?
This is what i have:
const [isCorrect, setIsCorrect] = useState(false);
const [currentTaskIndex, setCurrentTaskIndex] = useState(0);
useEffect(() => {
const checkIfCorrect = newData.map((data) => {
....
});
}, [!isCorrect ? newItems : !newItems]);
// Filtered task
useEffect(() => {
const filterObject = newData.filter((data) => {
return data.map((dataItems) => {
return dataItems.map((tasks, i) => {
return Object.entries(tasks)[currentTaskIndex].map((task, i) => {
setFilteredTasks(task);
});
});
});
});
}, [newData]);
useEffect(() => {
isCorrect && setCurrentTaskIndex((prev) => prev + 1);
setNewItems([]);
setIsCorrect(false);
const filterObject = newData.filter((data) => {
return data.map((dataItems) => {
return dataItems.map((tasks, i) => {
return Object.entries(tasks)[currentTaskIndex].map((task, i) => {
setFilteredTasks((currTask) => [...currTask, task]);
});
});
});
});
}, [isCorrect]);
{filteredTasks.map((taskItems, i) => (
<View key={i}>
{taskItems.en?.map((enItems, i) => (
.....
))}
</View>
React components automatically re-render whenever there is a change in their state or props. A simple update of the state, from anywhere in the code, causes all the User Interface (UI) elements to be re-rendered automatically.
So, by adding just currentTaskIndex on dependency array your useEffect also re-render.
when you just put newData on dependency array its could not update UI because newData is not a state or Props.
Yes this is your solution:
// Filtered task
useEffect(() => {
const filterObject = newData.filter((data) => {
return data.map((dataItems) => {
return dataItems.map((tasks, i) => {
return Object.entries(tasks)[currentTaskIndex].map((task, i) => {
setFilteredTasks(task);
});
});
});
});
}, [newData, currentTaskIndex]); <------
An finally fixed!
by just adding CurrentTaskIndex as dependency to Filtered task useEffects
// Filtered task
useEffect(() => {
const filterObject = newData.filter((data) => {
return data.map((dataItems) => {
return dataItems.map((tasks, i) => {
return Object.entries(tasks)[currentTaskIndex].map((task, i) => {
setFilteredTasks(task);
});
});
});
});
}, [newData, currentTaskIndex]); <------

What is the correct way to use react-native-sound?

When using the RNSound library, I ran into a problem - I can't pause the sound.
initialize :
Sound.setCategory("Playback");
let whoosh = new Sound("complite.mp3", Sound.MAIN_BUNDLE, (error) => {
if (error) {
console.log("failed to load the sound", error);
return;
}
})
and use like this:
<Button
onPress={() => {
if (!start) {
whoosh.play();
const myTimer = setInterval(() => {
setCounter((counter) => counter - 1);
}, 1000);
setTimer(myTimer);
setStart((start) => !start);
} else {
whoosh.pause();
clearInterval(timer);
setCounter(null);
setStart((start) => !start);
}
}}
the first time the button is pressed, the sound is played. On the second press, nothing happens and music is playing. On the third press, the same melody runs in parallel for the second time. As far as I understand, each time I click on the button, I refer to a new instance of Sound. Help with a solution please.
p.s. - i need solution with functional component, not a class. Thanks.
Declare the Sound instance outside of your component scope. You don't need to create a new instance of Sound everytime. Refer my sample.
Sound.setCategory('Playback');
var whoosh = new Sound('beep.mp3', Sound.MAIN_BUNDLE, error => {
if (error) {
console.log('failed to load the sound', error);
return;
}
// loaded successfully
console.log(
'duration in seconds: ' +
whoosh.getDuration() +
'number of channels: ' +
whoosh.getNumberOfChannels(),
);
});
const App: () => Node = () => {
const [start, setStart] = useState(false);
const [counter, setCounter] = useState(0);
const [timer, setTimer] = useState(null);
return (
<Button
onPress={() => {
if (!start) {
whoosh.play();
const myTimer = setInterval(() => {
setCounter(counter => counter - 1);
}, 1000);
setTimer(myTimer);
setStart(start => !start);
} else {
whoosh.pause();
clearInterval(timer);
setCounter(null);
setStart(start => !start);
}
}}
title="Click me"
/>
);
};
Let me know how it goes.

After a button is pressed to start a setInterval(), how do I clearInterval once a condition is met

I have an app that starts an interval when a button is pressed.
I want to stop the interval after a state reaches 5.
I have tried adding the if condition in UseEffect, and tried putting the condition within the function itself, but both don't seem to work. The console.log(sequence) does print successfully in useEffect I can see that the sequence does indeed increase, but it keeps increasing beyond 5 and never clearInterval.
const [sequence, setSequence] = useState(0)
const interval = () =>{
setInterval(() => {
setSequence((prevSequence) => (
prevSequence+1
))
}, 2000);
}
const plant = ()=>{
interval()
console.log(sequence)
if(sequence>5){
clearInterval (interval)
}
}
useEffect(() => {
console.log(sequence)
if(sequence>5){
return () => clearInterval(interval);
}
}, [sequence]);
return(
<View style = {styles.container} >
<Image
style={{height:500,width:300,}}
source= {tomato[sequence]}
/>
<Button
title = 'Fertilize Plant'
style= {styles.text}
onPress= {plant}>
</Button>
</View>)
}
Issue
You are not clearing the interval correctly.
const interval = () => {
setInterval(() => {
setSequence((prevSequence) => prevSequence + 1)
}, 2000);
};
...
useEffect(() => {
console.log(sequence);
if (sequence > 5) {
return () => clearInterval(interval);
}
}, [sequence]);
Here interval is a reference to the function, not the actual interval timer id returned from setInterval.
Solution
Store the interval timer id in a React ref to be referenced around the component.
const intervalRef = useRef();
const interval = () => {
intervalRef.current = setInterval(() => {
setSequence((prevSequence) => prevSequence + 1)
}, 2000);
};
...
useEffect(() => {
const intervalId = intervalRef.current;
// also clear on component unmount
return () => clearInterval(intervalId);
}, []);
useEffect(() => {
if (sequence > 5) {
clearInterval(intervalRef.current);
}
}, [sequence]);
Also, there's no need to check the sequence value and clear the interval in the plant callback, the useEffect hook with dependency on sequence will handle that. Plant only needs to start the interval.
const plant = () => {
interval();
};
You can use the clearInterval function to stop the interval you had set.
Here is an example;
let counter = 0;
const interval = setInterval(() => {
counter++;
console.log(`Counter = ${counter}`);
if (counter >= 3) {
console.log("Interval Stopped");
clearInterval(interval);
}
}, 1000);

React Native unmounted screen renders when Redux state changed

I'm using React Native with Redux. Currently i'm having two screens that uses same redux state. Two screen are on a stack navigation. There is a use effect call in the 1st screen that trigger when the redux state changed. same kind a use effect call is also in the 2nd screen that trigger when same redux state changed. The problem is when i navigate from screen 1 to screen 2 and changed the state, it triggers both use effects.
This is 1st Screen. I'm navigating to 2nd screen using navigation.navigate('LoginScreen')
export default function StartScreen({route, navigation, props}) {
const {loginStatus,user} = useSelector(state => state.auth);
const initialRender = useRef(true);
useEffect(() => {
console.log('use effect');
if (!initialRender.current) {
if (loginStatus === 'success') {
hideDialog();
navigation.reset({
index: 0,
routes: [{name: 'Dashboard'}],
});
} else if(loginStatus === 'failed') {
alert('invalid QR code!')
hideDialog();
dispatch(clearLoginStatus())
}
} else {
initialRender.current = false;
}
}, [loginStatus]);
return (
<Background maxWidth="85%">
<VectorHeading
img={require('../assets/startvector.png')}
marginBottom={50}
/>
<Header>xxxxxxx</Header>
<Paragraph>
The easiest way to start with your amazing application.
</Paragraph>
<Button mode="contained" onPress={() => navigation.navigate('QRScanner')}>
Scan QR
</Button>
<Button mode="contained" onPress={() => navigation.navigate('LoginScreen')}>
Login
</Button>
<Loader visible={visible} hideDialog={hideDialog} />
</Background>
);
.........
This is 2nd Screen
export default function LoginScreen({navigation, props}) {
....
const {isLoggedIn, loginStatus,user} = useSelector(state => state.auth);
const initialRender = useRef(true);
useEffect(() => {
console.log('use effect');
if (!initialRender.current) {
if (loginStatus === 'success') {
hideDialog();
forward();
} else if(loginStatus === 'failed') {
alert('invalid credentials!')
hideDialog();
dispatch(clearLoginStatus())
}
} else {
initialRender.current = false;
}
}, [loginStatus]);
.........
When "loginStatus" changed it rendering useefect functions in both screens and show both alerts that setted inside useeffect.
Any help will be really appreciated.
Try adding another useEffect to clean up when component unmounts,
useEffect(() => {
return () => {
console.log("cleaned up");
};
}, []);

wait for useState to set values ​and then call function

I'm using react native without expo, when trying to set a value with UseState it doesn't set immediately, and I can't get the values ​​in another function.
const [gridData, setGridData] = useState([]);
useEffect(() => {
getGridApi().then((response)=>{
setGridData(response);
pressed('Mon', 0);
})
}, []);
const pressed = async (category, index) => {
console.log(gridData); // empty
}
How can I make it wait to set and then call the function pressed()
you can use this package or you can your own custom hook for this. unfortunately react don't provide useState With Callback functionality
Example:
import useStateWithCallback from 'use-state-with-callback';
const App = () => {
const [count, setCount] = useStateWithCallback(0, count => {
if (count > 1) {
console.log('Threshold of over 1 reached.');
} else {
console.log('No threshold reached.');
}
});
return (
<div>
<p>{count}</p>
<button type="button" onClick={() => setCount(count + 1)}>
Increase
</button>
</div>
);
};
Cioa, unfortunately with hooks, setting state is async and you cannot get the last value in this way. But you can use another useEffect hook to retrieve any changes of state variable.
Try this:
useEffect(() => {
console.log(gridData); // this shows the last value of gridData setted by the other useEffect
}, [gridData]);
But pay attention: this useEffect I worte will be triggered every time gridData changes his value!