listen to deeply nested react state - javascript

I am currently working on a music player in React.
So far I have a Context Provider with a music element stored with the useState hook.
const [currentSong, setCurrentSong] = useState(null);
useEffect(() => {
fetchSong();
}, []);
const fetchSong = () => {
const songAudio = new Audio(`localhost/song/13/audio`)
songAudio.onloadeddata = () => {
songAudio.play();
setCurrentSong(songAudio);
}
}
After that the currentSong Object looks something like this
<audio preload="auto" src="http://localhost/song/13/audio">
{...}
duration: 239.081
currentTime: 113.053
​{...}
<prototype>: HTMLAudioElementPrototype { … }
Because the song is playing the currentTime gets updated automatically.
My question is if it is possible to trigger a rerender every time currentTime changes so that I can update a span element with that number.
The span is in a seperate file and consumes the Context Provider which provides the currentSong object.
const { currentSong, {...} } = useMusicContext();
{...}
return (
<span className='...'>
{currentSong? currentSong.currentTime: "0:00"}
</span>
)
The problem is that the component does not know that the currentTime value changed and only updates the text if a rerender is triggered by something else.

Add an event listener to the audio element for timeupdate events and use those to update your state (or whatever).
Here's a quick demo implementation. Source included below for easier reference.
// Audio component to handle attaching the listener
import { useEffect, useRef } from "react";
export function Audio({ onTimeUpdate, ...props }) {
const audioRef = useRef();
useEffect(() => {
const { current } = audioRef;
current?.addEventListener("timeupdate", onTimeUpdate);
return () => current?.removeEventListener("timeupdate", onTimeUpdate);
}, [audioRef, onTimeUpdate]);
return (
<audio ref={audioRef} {...props} />
);
}
export default function App() {
const [time, setTime] = useState();
const onTimeUpdate = (e) => {
setTime(e.target.currentTime);
};
return (
<div className="App">
<Audio onTimeUpdate={onTimeUpdate} controls src="./audio-sample.mp3" />
<div>{time}</div>
</div>
);
}

Tough to exactly say what to do here - would need more info/code, but I do believe that passing down currentTime as a prop would work.
If that is not possible, or you don't want to keep passing down props, you may want to look into the react hook called useContext.
Alternatively, perhaps you could use useEffect to trigger re-renders in the component you want to update. Not exactly sure how you would trigger this re-render/what you would put in the dependency array without more info.

Related

Get DOM Element from React.createElement()

Is there a way to get basic DOM Element from React.createElement?
Like I'm trying to create a list of React audio elements for each participant in the conversation and I need to attach a track to an element, but it's not working with react elements...
My idea is something like this, but this is not working
const ref = useRef<HTMLAudioElement>()
const addAudioTrack = (track: AudioTrack) => {
const audio = React.createElement("audio", {key: track.name, ref: ref})
console.log(ref.current)
track.attach(ref.current)
setAudioTracks((prevTracks: any) => [...prevTracks, audio])
}
EDIT: reproducible example can't be totally provided because for "track" you need Twilio but here is something that you can try... I just want to know if there is a possibility to get react DOM element from ReactElement or I need to use another approach
import React, {useRef, useState} from "react";
const NewTest = () => {
const [audioTracks, setAudioTracks] = useState<any>([])
const ref = useRef<HTMLAudioElement>()
const addAudioTrack = (track: any) => {
const audio = React.createElement("audio", {key: track.name, ref: ref})
console.log(ref.current)
if(ref.current) console.log("it is working")
// track.attach(ref.current)
setAudioTracks((prevTracks: any) => [...prevTracks, audio])
}
return (
<div>
<button onClick={() => {
addAudioTrack({name: `audioTrack-${(((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1)}`})
}}>
AddTrack
</button>
{audioTracks && audioTracks.map((audio: any) => {
return <div key={audio.key} style={{width: 50, height: 50, backgroundColor: "red"}}>{audio} {audio.key}</div>
})}
</div>
)
}
export default NewTest
Twilio developer evangelist here.
I think you might be thinking of this the wrong way. You do need a ref to use track.attach, but you can still handle the creation of elements via JSX.
I'd create an <AudioTrack> element that you can render with each audio track that uses useRef and useEffect to get an <audio> element and use track.attach. Something like this:
import React, { useEffect, useRef } from "react";
const AudioTrack = ({ audioTrack }) => {
const ref = useRef<HTMLAudioElement>(null);
useEffect(() => {
audioTrack.attach(ref.current);
return () => {
audioTrack.detach();
}
}, [audioTrack])
return <div><audio ref={ref}></div>;
}
export AudioTrack;
Then, in the parent container, you can render an <AudioTrack> for each of the audioTracks in your state.
I walk through how I created a Twilio Video app using React Hooks in this blog post, that might be helpful too.

Context Api state is not changing

im trying to call a function called deleteTask inside the Context Provider, from a component that consumes the context using the useContext hook, which deletes a certain item from an array in the state of the context provider, but when i do it, the state of the provider doesnt change at all, i try to follow the problem and the function excecutes but it seems like if it was excecuting in the scope of a copied Provider? Also tried a function to add a task and im having the same issue. I also added a function to set the active task, and i dont know why that one did work, while the others dont. I dont really know whats happening, here is the code, pleeeeease help me:
tasks-context.jsx
import React, { useState } from 'react';
import { useEffect } from 'react';
const dummyTasks = [{
task: {
text: 'hello',
},
key: 0,
isActive: false
},
{
task: {
text: 'hello 2',
},
key: 1,
isActive: false
}];
export const TasksContext = React.createContext({ });
export const TasksProvider = ( props ) => {
const [ tasks, setTasks ] = useState( dummyTasks );
const [ activeTask, setActiveTask ] = useState();
//NOT WORKING
const deleteTask = ( taskToDeleteKey ) =>{
setActiveTask( null );
setTasks( tasks.filter( task => task.key !== taskToDeleteKey ));
};
//THIS ONE WORKS (??)
const handleSelectTask = ( taskToSelect, key ) =>{
setActiveTask( taskToSelect );
const newTaskArray = tasks.map( task => {
if( task.key === key ){
task.isActive = true;
}else{
ficha.isActive = false;
}
return task;
});
setTask( newTaskArray );
};
return ( <TasksContext.Provider
value={{ tasks,
activeTask,
addTask,
deleteTask,
handleSelectTask}}>
{props.children}
</TasksContext.Provider>
);
};
the "main"
Main.jsx
import React from 'react';
import './assets/styles/gestion-style.css';
import './assets/styles/icons.css';
import { TasksProvider } from '../../Context/tasks-context';
import TaskContainer from './components/taskContainer.jsx';
function Main( props ) {
return (
<TasksProvider>
<TaskContainer />
</TasksProvider>
);
}
the task container maps the array of tasks:
TaskContainer.jsx
import React, { useContext, useEffect } from 'react';
import TaskTab from './TaskTab';
import { TasksContext } from '../../Context/tasks-context';
function TaskContainer( props ) {
const { tasks } = useContext( TasksContext );
return (
<div className="boxes" style={{ maxWidth: '100%', overflow: 'hidden' }}>
{tasks? tasks.map( taskTab=>
( <TaskTab task={taskTab.task} isActive={taskTab.isActive} key={taskTab.key} taskTabKey={taskTab.key} /> ))
:
null
}
</div>
);
}
export default TaskContainer;
And the task component from which i call the context function to delete:
TaskTab.jsx
import React, { useContext } from 'react';
import { TasksContext } from '../../Context/tasks-context';
function TaskTab( props ) {
let { task, isActive, taskTabKey } = props;
const { handleSelectTask, deleteTask } = useContext( TasksContext );
const selectTask = ()=>{
handleSelectTask( task, taskTabKey );
};
const handleDelete = () =>{
deleteTask( taskTabKey );
};
return (
<div onClick={ selectTask }>
<article className={`${task.type} ${isActive ? 'active' : null}`}>
<p className="user">{task.text}</p>
<button onClick={handleDelete}>
<i className="icon-close"></i>
</button>
</article>
</div>
);
}
export default TaskTab;
Thanks for the great question!
What is happening here is understandably confusing, and it took me a while to realize it myself.
TL;DR: handleSelectTask in the Provider is being called every time a button is clicked for deleteTask because of event propagation. handleSelectTask isn't using the state that has been modified by deleteTask, even though it's running after it, because it has closure to the initial tasks array.
Quick Solution 1
Stop the event from propagating from the delete button click to the TaskTab div click, which is probably the desired behavior.
// in TaskTab.jsx
const handleDelete = (event) => {
event.stopPropagation(); // stops event from "bubbling" up the tree
deleteTask(taskTabKey);
}
In the DOM (and emulated by React as well), events "bubble" up the tree, so that parent nodes can handle events coming from their child nodes. In the example, the <button onClick={handleDelete}> is a child of the <div onClick={selectTask}>, which means that when the click event is fired from the button, it will first call the handleDelete function like we want, but it will also call the selectTask function from the parent div afterwards, which is probably unintended. You can read more about event propagation on MDN.
Quick Solution 2
Write the state updates to use the intermediary state value at the time they are called.
// in tasks-context.jsx
const deleteTask = ( taskToDeleteKey ) => {
setActiveTask(null);
// use the function version of setting state to read the current value whenever it is run
setTasks((stateTasks) => stateTasks.filter(task => task.key !== taskToDeleteKey));
}
const handleSelectTask = ( taskToSelect, key ) =>{
setActiveTask( taskToSelect );
// updated to use the callback version of the state update
setTasks((stateTasks) => stateTasks.map( task => {
// set the correct one to active
}));
};
Using the callback version of the setTasks state update, it will actually read the value at the time the update is being applied (including and especially in the middle of an update!), which, since the handleSelectTask is called after, means that it actually sees the array that has already been modified by the deleteTask that ran first! You can read more about this callback variant of setting state in the React docs (hooks) (setState). Note that this "fix" will mean that your component will still call handleSelectTask even though the task has been deleted. It won't have any ill-effects, just be aware.
Let's walk through what's happening in a bit more detail:
First, the tasks variable is created from useState. This same variable is used throughout the component, which is totally fine and normal.
// created here
const [ tasks, setTasks ] = useState( dummyTasks );
const [ activeTask, setActiveTask ] = useState();
const deleteTask = ( taskToDeleteKey ) =>{
setActiveTask( null );
// referenced here, no big deal
setTasks( tasks.filter( task => task.key !== taskToDeleteKey ));
};
const handleSelectTask = ( taskToSelect, key ) =>{
setActiveTask( taskToSelect );
// tasks is referenced here, too, awesome
const newTaskArray = tasks.map( task => {
if( task.key === key ){
task.isActive = true;
}else{
task.isActive = false;
}
return task;
});
setTasks( newTaskArray );
};
Where the trouble comes in, is that if both of the functions are trying to update the same state value in the same render cycle, they will both be referencing the original value of the tasks array, even if the other function has attempted to update the state value! In your case, because the handleSelectTask is running after deleteTask, this means that handleSelectTask will update state using the array that hasn't been modified! When it runs, it will still see two items in the array, since the tasks variable won't change until the update is actually committed and everything rerenders. This makes it look like the delete portion isn't functioning, when really its effect is just being discarded since handleSelectTask isn't aware that the delete happened before it.
Lucas, this is not an issue with Context or Provider.
The problem that you are facing is actually a mechanism known as event bubbling where the current handler executes followed by parent handlers.
More info on event bubbling could be found here. https://javascript.info/bubbling-and-capturing.
In your case first, the handleDelete function gets called followed by handleSelect function.
Solution: event.stopPropagation();
Change your handleDelete and handleSelect function to this
const selectTask = () => {
console.log("handle select called");
handleSelectTask(task, taskTabKey);
};
const handleDelete = event => {
console.log("handle delete called");
event.stopPropagation();
deleteTask(taskTabKey);
};
Now check your console and you will find only the handle delete called will print and this would solve your problem hopefully.
If it still doesn't work then do let me know. I will create a codesandbox version for you.
Happy Coding.

How replace componentWillReceiveProps with hooks [duplicate]

This question already has answers here:
React re-write componentWillReceiveProps in useEffect
(4 answers)
Closed 2 years ago.
i wonder how using useEffect like componentWillReceiveProps.
i'm using redux in my react app.
So i have a some redux state when it state updated i wan't to execute some function in my component. When i use class components i did it like that:
componentWillReceiveProps(nextProps) {
if (nextProps.Reducer.complete !== this.props.Reducer.complete) {
someFunc();
}
}
Now i'm using just functional components and hooks.
now my component is like that: I'm trying to do it with this way but not working. Any idea where i mistaken ?
function Component(props) {
const Reducer = useSelector(state => state.Reducer);
const someFunc = () => {
.....
}
useEffect(() => {
someFunc();
}, [Reducer.complete]);
}
export default Component;
Since someFunc is a dependency of the effect and you create someFunc every time you render Component the effect will either be called every time because you correctly added the dependency or behave unexpectedly because you didn't add it or you have set up your development environment with eslint and exhaustive deps and your dev environment will warn you that your effect has missing dependencies.
To prevent someFunc to be re created on every render you can use useCallback:
function Component(props) {
const Reducer = useSelector(state => state.Reducer);
const someFunc = useCallback(() => {
// .....
}, []);
useEffect(() => {
someFunc(Reducer.complete); //should use the dep
}, [Reducer.complete, someFunc]);
}
If in the body of someFunc you use props or values created in Component then passing [] as dependencies will not work, you can use exhaustive deps to help you with this.
Here is a working example of effect reacting to changing value:
const {
useCallback,
useState,
useEffect,
} = React;
function Component() {
const [complete, setComplete] = React.useState(false);
const [message, setMessage] = React.useState(
`complete is ${complete}`
);
const completeChanged = useCallback(complete => {
console.log(
'running effect, setting message:',
complete
);
setMessage(`complete is ${complete}`);
}, []);
const toggleComplete = useCallback(
() => setComplete(c => !c),
[]
);
useEffect(() => {
completeChanged(complete); //should use the dep
}, [complete, completeChanged, setMessage]);
console.log('rendering:', complete);
return (
<div>
<button onClick={toggleComplete}>
toggle complete
</button>
{message}
</div>
);
}
//render app
ReactDOM.render(
<Component />,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Ok, so I assume that Reducer now is an object and have a complete key.
Try to create conditional inside your useEffect.
useEffect(() => {
if (Reducer.complete) {
runThisFunction()
}
}, [Reducer.complete, runThisFuntion])
The react hook equivalent to the old componentWillReceive props can be done using the useEffect hook, just specifying the prop that we want to listen for changes in the dependency array.
Try this:
useEffect( () => {
someFunc()
}, [props.something, someFunc])

How to rerender when refs change

Code:
import DrawControl from "react-mapbox-gl-draw";
export default function MapboxGLMap() {
let drawControl = null
return(
<DrawControl ref={DrawControl => {drawControl = DrawControl}}/>
)
}
I want to load data when the drawControl not null. I check the document that may use callback ref.
So, how do I listen the drawControl changes and load data?
If you want to trigger a re-render after the ref changes, you must use useState instead of useRef. Only that way can you ensure that the component will re-render. E.g.:
function Component() {
const [ref, setRef] = useState();
return <div ref={newRef => setRef(newRef)} />
}
As described under useRef documentation:
Keep in mind that useRef doesn’t notify you when its content changes. Mutating the .current property doesn’t cause a re-render. If you want to run some code when React attaches or detaches a ref to a DOM node, you may want to use a callback ref instead.
It may sometimes be better to store whatever value you are getting from the DOM node, as suggested here, instead of storing the node itself.
useCallback could listen the ref changed
export default function MapboxGLMap() {
const drawControlRef = useCallback(node => {
if (node !== null) {
//fetch(...) load data
}
},[]);
return(
<DrawControl ref={drawControlRef }/>
)
}
You can use a callback function that useEffect base on the change in useRef
function useEffectOnce(cb) {
const didRun = useRef(false);
useEffect(() => {
if(!didRun.current) {
cb();
didRun.current = true
}
})
}

Using useEffect with event listeners

The issue I'm having is that when I set up an event listener, the value the event listener sees doesn't update with the state. It's as if it's bound to the initial state.
What is the correct way to do this?
Simple example:
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
const App = () => {
const [name, setName] = useState("Colin");
const [nameFromEventHandler, setNameFromEventHandler] = useState("");
useEffect(() => {
document.getElementById("name").addEventListener("click", handleClick);
}, []);
const handleButton = () => {
setName("Ricardo");
};
const handleClick = () => {
setNameFromEventHandler(name);
};
return (
<React.Fragment>
<h1 id="name">name: {name}</h1>
<h2>name when clicked: {nameFromEventHandler}</h2>
<button onClick={handleButton}>change name</button>
</React.Fragment>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Gif below, since SO code snippet doesn't work for some reason.
So your problem is that you pass an empty array as the second argument to your effect so the effect will never be cleaned up and fired again. This means that handleClick will only ever be closed over the default state. You've essentially written: setNameFromEventHandler("Colin"); for the entire life of this component.
Try removing the second argument all together so the effect will be cleaned up and fired whenever the state changes. When the effect refires, the function that will be handling the click event that will be closed over the most recent version of your state. Also, return a function from your useEffect that will remove your event listener.
E.g.
useEffect(() => {
document.getElementById("name").addEventListener("click", handleClick);
return () => {
document.getElementById("name").removeEventListener("click", handleClick);
}
});
I think correct solution should be this: codesanbox. We are telling to the effect to take care about its dependency, which is the callback. Whenever it is changed we should do another binding with correct value in closure.
I believe the correct solution would be something like this:
useEffect(() => {
document.getElementById("name").addEventListener("click", handleClick);
}, [handleClick]);
const handleButton = () => {
setName("Ricardo");
};
const handleClick = useCallback(() => {
setNameFromEventHandler(name)
}, [name])
The useEffect should have handleClick as part of its dependency array otherwise it will suffer from what is known as a 'stale closure' i.e. having stale state.
To ensure the useEffect is not running on every render, move the handleClick inside a useCallback. This will return a memoized version of the callback that only changes if one of the dependencies has changed which in this case is 'name'.

Categories