Updating state without rendering the whole React component (useState) - reactjs

I have a component that instantiates a few classes from the Tone.js library (e.g audio players and filters) and defines a few functions acting on these objects, which are used as callbacks in a set of UI-rendered buttons (see relevant code below).
Two of these buttons are supposed to toggle the boolean state is3D using the useState hook (in the updateSpatial function) in order to enable/disable one of these buttons. However, this update obviously causes the component to re-render entirely, thus re-instantiating my classes, which prevent the defined functions to work afterwards.
In contrast, I also tried the useRef hook, which allows for is3D update without re-rendering, but the button's disabled state is not updated as the component does not re-render.
Is there a pattern that fits with this situation? My next attempts include using HOC, Context, or Redux, but I'm not sure this is the most straighforward. Thanks!
import React, {useState, useEffect, Fragment} from 'react'
import { Player, Destination} from "tone";
const Eagles = () => {
const [is3D, setIs3D] = useState(false);
useEffect(() => {
// connect players to destination, ready to play
connectBase();
});
// Instantiating players
const player1= new Player({
"url": "https://myapp.s3.eu-west-3.amazonaws.com/Aguabeber.m4a",
"autostart": false,
"onload": console.log("player 1 ready")
});
const player2 = new Player({
"url": "https://myapp.s3.eu-west-3.amazonaws.com/Aguas.m4a",
"autostart": false,
"onload": console.log("player 2 ready")
});
// Functions
function connectBase() {
player1.disconnect();
player1.connect(Destination);
player2.disconnect();
player2.chain(Destination);
}
function playStudio() {
player1.volume.value = 0;
player2.volume.value = -60;
}
function playCinema() {
player1.volume.value = -60;
player2.volume.value = 0;
}
function playstereo() {
player1.volume.value = 0;
player2.volume.value = -60;
updateSpatial();
}
function playmyhifi() {
player1.volume.value = -60;
player2.volume.value = 0;
updateSpatial();
}
function start() {
player1.start();
player2.start();
playstereo();
}
function stop() {
player1.stop();
player2.stop();
console.log("stop pressed")
}
// Update state to toggle button enabled`
function updateSpatial() {
setIs3D((is3D) => !is3D);
}
return (
<Fragment>
<ButtonTL onClick={start}>Play</ButtonTL>
<ButtonTR onClick={stop}>Stop</ButtonTR>
<ButtonTL2 onClick={playstereo}>Stereo</ButtonTL2>
<ButtonTR2 onClick={playmyhifi}>3D</ButtonTR2>
<ButtonLL disabled={is3D} onClick={playStudio}>Studio</ButtonLL>
<ButtonLR onClick={playCinema}>Cinema</ButtonLR>
</Fragment>
)
}

I see two things wrong with your code.
First, everything within the "normal" body of the function will be executed on on every render. Hence, the need for states and hooks.
States allow you to keep data between renders, and hooks allow you to do a particular action upon a state change.
Second, useEffect(()=>console.log(hi)) does not have any dependencies, hence it will run on every render.
useEffect(()=>console.log(hi),[]) will execute only on the first render.
useEffect(()=>console.log(hi),[player1]) will execute when player1 changes.
useEffect(()=>console.log(hi),[player1, player2]) will execute when player1 OR player2 change.
Be careful with hooks with dependencies. If you set the state of one of the dependencies within the hook itself, it will create an infinite loop
Here is something closer to what you want:
import React, {useState, useEffect, Fragment} from 'react'
import { Player, Destination} from "tone";
const Eagles = () => {
const [is3D, setIs3D] = useState(false);
const [player1]= useState(new Player({
"url": "https://myapp.s3.eu-west-3.amazonaws.com/Aguabeber.m4a",
"autostart": false,
"onload": console.log("player 1 ready")
}));
const [player2] = useState(new Player({
"url": "https://myapp.s3.eu-west-3.amazonaws.com/Aguas.m4a",
"autostart": false,
"onload": console.log("player 2 ready")
}));
useEffect(() => {
// connect players to destination, ready to play
connectBase();
},[player1, player2]);
// Instantiating players
// Functions
function connectBase() {
player1.disconnect();
player1.connect(Destination);
player2.disconnect();
player2.chain(Destination);
}
function playStudio() {
player1.volume.value = 0;
player2.volume.value = -60;
}
function playCinema() {
player1.volume.value = -60;
player2.volume.value = 0;
}
function playstereo() {
player1.volume.value = 0;
player2.volume.value = -60;
updateSpatial();
}
function playmyhifi() {
player1.volume.value = -60;
player2.volume.value = 0;
updateSpatial();
}
function start() {
player1.start();
player2.start();
playstereo();
}
function stop() {
player1.stop();
player2.stop();
console.log("stop pressed")
}
// Update state to toggle button enabled`
function updateSpatial() {
setIs3D((is3D) => !is3D);
}
return (
<Fragment>
<ButtonTL onClick={start}>Play</ButtonTL>
<ButtonTR onClick={stop}>Stop</ButtonTR>
<ButtonTL2 onClick={playstereo}>Stereo</ButtonTL2>
<ButtonTR2 onClick={playmyhifi}>3D</ButtonTR2>
<ButtonLL disabled={is3D} onClick={playStudio}>Studio</ButtonLL>
<ButtonLR onClick={playCinema}>Cinema</ButtonLR>
</Fragment>
)
}

Related

Run React's useEffect at random intervals

I am new to React and ThreeJs. I am using react-three-fiber to animate a 3d model.
I have generated a React component that uses `useEffect to trigger some animations.
This code runs in an infinite loop it seems; I would like for the animation to run once, pause for a random number of seconds between 1 and 9, and then for the animation to run again.
import React, { useRef, useEffect } from 'react'
import { useGLTF, useAnimations } from '#react-three/drei'
export default function Model({ ...props }) {
const group = useRef()
const { nodes, materials, animations } = useGLTF('/blob.glb')
const { actions } = useAnimations(animations, group)
useEffect(() => {
console.log(actions)
actions.someAction.play()
});
return (
<group ref={group} {...props} dispose={null}>
<group position={[0.16, 0.21, 0]} scale={[1.13, 0.79, 1.13]}>
<mesh
...
How can I modify this so that the animations run at random intervals?
Without seeing the rest of your code, I can't say why it's running in an infinite loop. However, you're not passing [actions] as a dependency to your effect (in fact, you're not passing anything as a dependency) - which means that effect will run every time the component renders.
To get the result you're chasing though, I'd probably create a custom hook that takes care of the "re-run after a random delay" logic for you; something like this:
const useRandomlyRepeatedEffect = (effect, deps) => {
// Keep track of the currently running timeout so we can clear it
// if the component unmounts
const timeoutRef = useRef();
useEffect(() => {
const runAndWait = () => {
effect();
const delaySecs = Math.floor(Math.random() * 10);
timeoutRef.current = setTimeout(runAndWait, delaySecs * 1_000);
};
runAndWait();
// Cancel the timeout when the effect if the component unmounts.
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = undefined;
}
};
}, deps);
};
You don't have to do this - you could just have that inline in your component, but I'm a big fan of encapsulating custom logic in hooks.
Then, instead of your useEffect, you should be able to substitute it with useRandomlyRepeatedEffect:
export default const Model = (props) => {
// const actions = /* .... */
useRandomlyRepeatedEffect(() => {
actions.someAction.play;
}, [actions]);
};
Note here that [actions] is being supplied as a dependency to the effect.

Cannot update a component while rendering a different Component - ReactJS

I know lots of developers had similar kinds of issues in the past like this. I went through most of them, but couldn't crack the issue.
I am trying to update the cart Context counter value. Following is the code(store/userCartContext.js file)
import React, { createContext, useState } from "react";
const UserCartContext = createContext({
userCartCTX: [],
userCartAddCTX: () => {},
userCartLength: 0
});
export function UserCartContextProvider(props) {
const [userCartStore, setUserCartStore] = useState([]);
const addCartProduct = (value) => {
setUserCartStore((prevState) => {
return [...prevState, value];
});
};
const userCartCounterUpdate = (id, value) => {
console.log("hello dolly");
// setTimeout(() => {
setUserCartStore((prevState) => {
return prevState.map((item) => {
if (item.id === id) {
return { ...item, productCount: value };
}
return item;
});
});
// }, 50);
};
const context = {
userCartCTX: userCartStore,
userCartAddCTX: addCartProduct,
userCartLength: userCartStore.length,
userCartCounterUpdateCTX: userCartCounterUpdate
};
return (
<UserCartContext.Provider value={context}>
{props.children}
</UserCartContext.Provider>
);
}
export default UserCartContext;
Here I have commented out the setTimeout function. If I use setTimeout, it works perfectly. But I am not sure whether it's the correct way.
In cartItemEach.js file I use the following code to update the context
const counterChangeHandler = (value) => {
let counterVal = value;
userCartBlockCTX.userCartCounterUpdateCTX(props.details.id, counterVal);
};
CodeSandBox Link: https://codesandbox.io/s/react-learnable-one-1z5td
Issue happens when I update the counter inside the CART popup. If you update the counter only once, there won't be any error. But when you change the counter more than once this error pops up inside the console. Even though this error arises, it's not affecting the overall code. The updated counter value gets stored inside the state in Context.
TIL that you cannot call a setState function from within a function passed into another setState function. Within a function passed into a setState function, you should just focus on changing that state. You can use useEffect to cause that state change to trigger another state change.
Here is one way to rewrite the Counter class to avoid the warning you're getting:
const decrementHandler = () => {
setNumber((prevState) => {
if (prevState === 0) {
return 0;
}
return prevState - 1;
});
};
const incrementHandler = () => {
setNumber((prevState) => {
return prevState + 1;
});
};
useEffect(() => {
props.onCounterChange(props.currentCounterVal);
}, [props.currentCounterVal]);
// or [props.onCounterChange, props.currentCounterVal] if onCounterChange can change
It's unclear to me whether the useEffect needs to be inside the Counter class though; you could potentially move the useEffect outside to the parent, given that both the current value and callback are provided by the parent. But that's up to you and exactly what you're trying to accomplish.

React state not updating immediately

The React state is not updating immediately. I want to update the state immediately on the press of Play button.
import * as React from "react";
import { Button } from "react-native";
export default function Play() {
const [player, setPlayer] = React.useState(1);
function nextPlayer() {
setPlayer(2);
updateValue();
}
function updateValue() {
if (player == 1) {
console.log("player 1");
} else if (player == 2) {
console.log("player 2");
}
}
return <Button title="play" onPress={nextPlayer} />;
}
The function updateValue is created in the Closure that contains the "old value" of your state. If you want it to run with the new values, use an useEffect hook or pass the current value as an argument.
const [value, setValue] = useState();
useEffect(() => {
// receives the latest value and is called on every change (also the first one!)
}, [value])

React local element not updating?

I have a component which has a local variable
let endOfDocument = false;
And I have a infinite scroll function in my useEffect
useEffect(() => {
const { current } = selectScroll;
current.addEventListener('scroll', () => {
if (current.scrollTop + current.clientHeight >= current.scrollHeight) {
getMoreExercises();
}
});
return () => {
//cleanup
current.removeEventListener('scroll', () => {});
};
}, []);
In my getMoreExercises function I check if we reached the last document in firebase
function getMoreExercises() {
if (!endOfDocument) {
let ref = null;
if (selectRef.current.value !== 'All') {
ref = db
.collection('exercises')
.where('targetMuscle', '==', selectRef.current.value);
} else {
ref = db.collection('exercises');
}
ref
.orderBy('average', 'desc')
.startAfter(start)
.limit(5)
.get()
.then((snapshots) => {
start = snapshots.docs[snapshots.docs.length - 1];
if (!start) endOfDocument = true; //Here
snapshots.forEach((exercise) => {
setExerciseList((prevArray) => [...prevArray, exercise.data()]);
});
});
}
}
And when I change the options to another category I handle it with a onChange method
function handleCategory() {
endOfDocument = false;
getExercises();
}
I do this so when we change categories the list will be reset and it will no longer be the end of the document. However the endOfDocument variable does not update and getMoreExercise function will always have the endOfDocument value of true once it is set to true. I cannot change it later. Any help would be greatly appreciated.
As #DevLoverUmar mentioned, that would updated properly,
but since the endOfDocument is basically never used to "render" anything, but just a state that is used in an effect, I would put it into a useRef instead to reduce unnecessary rerenders.
Assuming you are using setExerciseList as a react useState hook variable. You should use useState for endOfDocument as well as suggested by Brian Thompson in a comment.
import React,{useState} from 'react';
const [endOfDocument,setEndOfDocument] = useState(false);
function handleCategory() {
setEndOfDocument(false);
getExercises();
}

How to check my conditions on react hooks

I need check my conditions on react hooks when startup or change value
I try that by this code but I cant run without Btn
import React,{useEffect} from 'react';
import { StyleSheet, View } from 'react-native';
import * as Permissions from 'expo-permissions';
import { Notifications} from 'expo';
export default function NotificationsTest() {
const y = 5;
const askPermissionsAsync = async () => {
await Permissions.askAsync(Permissions.USER_FACING_NOTIFICATIONS);
};
const btnSendNotClicked = async () => {
if(y===5){
await askPermissionsAsync();
Notifications.presentLocalNotificationAsync({
title: "Title",
body: "****** SUBJ *******",
ios:{
sound:true,
},
android:{
sound:true,
color:'#512da8',
vibrate:true,
}
});
}else{
} }
useEffect(()=>{
return btnSendNotClicked
}
,[])
return (
<View style={styles.container}>
</View>
);
}
and I just want to confirm is it good practice to checking this kind of condition in useEffect ?
Hi on startup useEffect with empty array will be fired and this will be happened during the page load (first time), so you can write your condition there. here is an example:
useEffect(()=> {
// Your condition here
}, []);
If you have a variable like value then you can write another useEffect like below and set that variable (value) in second parameter of useEffect as an array
const [value, setValue] = useState('');
useEffect(()=> {
// Your condition here
}, [value]);
const y = 5;
//this one will run every time the component renders
//so you could use it to check for anything on the startup
useEffect(()=> {
// Your condition here
}, []);
Otherwise you could add a value or array of values that you want to track for changes so this useEffect bellow Will only run if the y const changes so you could add an if check there to check if y===5 also this useEffect will run on the first initial of the const y so its perfect in your case
}
useEffect(()=> {
// will run every time y const change and on the first initial of y
if (y === 5)
{
//do your stuff here
}
}, [y]);

Resources