I have the following function component:
const Player = () => {
const [spotifyPlayer, setSpotifyPlayer] = React.useState<SpotifyPlayer | null>(null);
return (
<SongControls togglePlay={togglePlay} isPlaying={spotifyPlayer.isPlaying} />
);
};
the SongControls component is responsible for showing the correct control button (start/pause):
const SongControls: React.FC<Props> = ({ isPlaying }: Props) => {
return (
{isPlaying ? (
<Pause style={iconBig} onClick={togglePlay} />
) : (
<PlayArrow style={iconBig} onClick={togglePlay} />
)}
);
};
the spotifyPlayer state in the Player component is a class that has a field isPlaying of type boolean. It also has a function defined togglePlay() which toggles isPlaying.
The problem is that when togglePlay() is called React doesn't rerender the components. I understand why (since the instance of SpotifyPlayer is not changing, thats why you shouldn't update react state directly).
I can't call setSpotifyPlayer() to update the state because of 2 reasons:
the instance of SpotifyPlayer is responsible for toggling isPlaying
when I change the state use js deconstructing I lose the prototype of the spotifyPlayer state and the function won't be available anymore.
I am not able to figure this out.
Your understanding of the problem is correct. You cannot rely on changes to the internal state of the SpotifyPlayer object to trigger a re-render. I recommend using a local state to store the value of isPlaying and update the SpotifyPlayer based on changes to that value.
If the player object has some sort of onChange listener, then I would use the player to set the local state rather than the other way around.
I'm not sure what package the player comes from, so I am assuming this interface:
class SpotifyPlayer {
isPlaying: boolean = false;
play(): void {
this.isPlaying = true;
}
pause(): void {
this.isPlaying = false;
}
}
We can use useRef to store a consistent reference to a player object.
const spotifyPlayer = React.useRef<SpotifyPlayer>(new SpotifyPlayer()).current;
We will store the value of isPlaying in local state. We can get the initial value from the player.
const [isPlaying, setIsPlaying] = React.useState(spotifyPlayer.isPlaying);
In order to keep isPlaying in sync with the player, we could use useEffect.
const togglePlay = () => setIsPlaying(!isPlaying);
React.useEffect(() => {
if ( isPlaying && ! spotifyPlayer.isPlaying ) {
spotifyPlayer.play();
} else if ( ! isPlaying && spotifyPlayer.isPlaying ) {
spotifyPlayer.pause();
}
}, [spotifyPlayer, isPlaying]);
But if togglePlay is the only function which will change the value, then we could handle the changes in that function.
const togglePlay = () => {
if (isPlaying ) {
spotifyPlayer.pause();
setIsPlaying(false);
} else {
spotifyPlayer.play();
setIsPlaying(true);
}
}
Related
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.
I created one functional component and declared some variables in it.
const Foo = (props) => {
let instanceVariable = null;
const selectValue = (value) => {
instanceVariable = value;
}
const proccessVariable = () => {
// Use instanceVariable and do some task
}
return(
<SelectionOption onChange={selectValue} />
);
}
I observed that whenever parent component of Foo is re-rendered or sometime Foo itself re-renders instanceVariable set back to null. Instead of this I want to save selectedValue init and proccess it later on proccessVariable() method.
If I set instanceVariable as state it will work and there is no need of state to just hold the selected value.
I know useEffect(()=>{}, []) only run one time but how can I declare instanceVariable there and use it in other functions.
Can you please let me know what I'm doing wrong.
Since you declare a variable directly in your functional component, its value is reset everytime your component re-renders. You can make use of useRef to declare instance variables in functional component
const Foo = (props) => {
let instanceVariable = useRef(null);
const selectValue = (value) => {
instanceVariable.current = value; // make sure to use instanceVariable.current
}
const proccessVariable = () => {
// Use instanceVariable.current and do some task
}
return(
<SelectionOption onChange={selectValue} />
);
}
I've got some toggles that can be turned on/off. They get on/off state from a parent functional component. When a user toggles the state, I need to update the state in the parent and run a function.
That function uses the state of all the toggles to filter a list of items in state, which then changes the rendered drawing in a graph visualization component.
Currently, they toggle just fine, but the render gets out of sync with the state of the buttons, because the processing function ends up reading in old state.
I tried using useEffect(), but because the function has a lot of dependencies it causes a loop.
I tried coupling useRef() with useState() in a custom hook to read out the current state of at least the newest filter group that was set, but no luck there either.
Any suggestions on how I could restructure my code in a better way altogether, or a potential solution to this current problem?
Gross function that does the filtering:
function filterItems(threshold, items = {}) {
const { values } = kCoreResult;
const { coloredItems } = rgRef.current;
let itemsForUse;
let filteredItems;
if (Object.entries(items).length === 0 && items.constructor === Object) {
itemsForUse = baseItemsRef.current;
} else {
itemsForUse = items;
}
const isWithinThreshold = id => has(values, id) && values[id] >= threshold;
// filter for nodes meeting the kCoreValue criterion plus all links
filteredItems = pickBy(
itemsForUse,
(item, id) => !isNode(item) || isWithinThreshold(id)
);
filteredItems = pickBy(
filteredItems,
item =>
!has(item, 'data.icon_type') || !filterRef.current[item.data.icon_type]
);
setRg(rg => {
rg.filteredItems = leftMerge(filteredItems, coloredItems);
return {
...rg,
};
});
setMenuData(menuData => {
menuData.threshold = threshold;
return {
...menuData,
};
});
}
Function that calls it after button is pressed that also updates button state (button state is passed down from the filter object):
function changeCheckBox(id, checked) {
setFilter(filter => {
filter[id] = !checked;
return {
...filter,
};
});
filterItems(menuData.threshold);
}
It seems calling your filterItems function in the handler is causing the stale state bug, the state update hasn't been reconciled yet. Separate out your functions that update state and "listen" for updates to state to run the filter function.
Here's a demo that should help see the pattern:
export default function App() {
const [filters, setFilters] = useState(filterOptions);
const onChangeHandler = e => {
setFilters({ ...filters, [e.target.name]: e.target.checked });
};
const filterItems = (threshold, items = {}) => {
console.log("Gross function that does the filtering");
console.log("threshold", threshold);
console.log("items", items);
};
useEffect(() => {
filterItems(42, filters);
}, [filters]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
{Object.entries(filters).map(([filter, checked]) => {
return (
<Fragment key={filter}>
<label htmlFor={filter}>{filter}</label>
<input
id={filter}
name={filter}
type="checkbox"
checked={checked}
onChange={onChangeHandler}
/>
</Fragment>
);
})}
</div>
);
}
This works by de-coupling state updates from state side-effects. The handler updates the filters state by always returning a new object with next filter values, and the effect hook triggers on value changes to filters.
I've created a context to store values of certain components for display elsewhere within the app.
I originally had a single display component which would use state when these source components were activated, but this resulted in slow render times as the component was re-rendered with the new state every time the selected component changed.
To resolve this I thought to create an individual component for each source component and render them with initial values and only re-render when the source components values change.
i.e. for the sake of an example
const Source = (props) => {
const { name, some_data} = props;
const [setDataSource] = useContext(DataContext);
useEffect(() => {
setDataSource(name, some_data)
}, [some_data]);
return (
...
);
}
const DataContextProvider = (props) => {
const [currentState, setState] = useState({});
const setDataSource = (name, data) => {
const state = {
...currentState,
[name]: {
...data
}
}
}
return (
...
)
}
// In application
<Source name="A" data={{
someKey: 0
}}/>
<Source name="B" data={{
someKey: 1
}}/>
The state of my provider will look like so;
{
"B": {
"someKey": 1
}
}
I believe this is because setState is asynchronous, but I can't think of any other solution to this problem
You can pass the function to setState callback:
setState((state) => ({...state, [name]: data}))
It takes the latest state in argument in any case, so it always safer to use if your update depends on previous state.
I'm trying to use React hooks. I have a problem with this code:
class VideoItem extends Component {
handlePlayingStatus = () => {
this.seekToPoint();
...
}
seekToPoint = () => {
this.player.seekTo(30); // this worked - seek to f.ex. 30s
}
render() {
const { playingStatus, videoId } = this.state;
return (
<Fragment>
<ReactPlayer
ref={player => { this.player = player; }}
url="https://player.vimeo.com/video/318298217"
/>
<button onClick={this.handlePlayingStatus}>Seek to</button>
</Fragment>
);
}
}
So I want to get ref from the player and use a seek function on that. This works just fine but I have a problem to change it to hooks code.
const VideoItem = () => {
const player = useRef();
const handlePlayingStatus = () => {
seekToPoint();
...
}
const seekToPoint = () => {
player.seekTo(30); // this does not work
}
return (
<Fragment>
<ReactPlayer
ref={player}
url="https://player.vimeo.com/video/318298217"
/>
<button onClick={handlePlayingStatus}>Seek to</button>
</Fragment>
);
}
How can I remodel it?
From the documentation:
useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.
Thus your code should be:
player.current.seekTo(30);
(optionally check whether player.current is set)
useCallback might also be interesting to you.
useRef is a hook function that gets assigned to a variable, inputRef, and then attached to an attribute called ref inside the HTML element you want to reference.
Pretty easy right?
React will then give you an object with a property called current.
The value of current is an object that represents the DOM node you’ve selected to reference.
So after using useRef player contains current object, and inside current object, your all methods from vimeo player would be available(in your case seekTo(number))
so you should be using player.current.seekTo(30) instead.
refer this to know more about useRef.