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.
Related
Assume I have 3 queries x, y and z. I want to update the query x only when it is in view. Is there any way to identify it ?
I tried to maintain the view query key as a global state, but it seems not working fine. Is there any way to identify without maintaining viewing query key as global state.
Is there any possible way to get list of viewing queries ???
First of all, you need to hold the state of the component if it's in the viewport or not. The IntersectionObserver API allows you to detect when an element enters or leaves the viewport, which you can use to update the state of your component.
Then you can use that state as a key in your useQuery. Also you can use it in enabled option if you want to prevent refetch when item is not on the viewport.
import React, { useState, useEffect, useRef } from 'react';
import { useQuery } from 'react-query';
function Test() {
const [isInViewport, setIsInViewport] = useState(false);
const ref = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setIsInViewport(true);
} else {
setIsInViewport(false);
}
});
});
observer.observe(ref.current);
return () => {
observer.unobserve(ref.current);
};
}, []);
useQuery(
[isInViewport],
() => (
/// your query
),
{
enabled: isInViewport,
},
);
return (
<div ref={ref}>
<div>{isInViewport ? 'In viewport' : 'Not in viewport'}</div>
</div>
);
}
export default Test;
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.
I am using React-Flow in order to visualise a tree hierarchy of components in React. And whenever you create a custom node in React-Flow, you use it in the tree like that:
<ReactFlow
onLoad={onLoadTree}
elements={nodesAndEdges}
nodeTypes={{ reactComponent: ComponentNode }}
Which makes it impossible to pass props. Let's say that I want to pass something, then I would be able to do that if the syntax was like reactComponent: <ComponentNode myProps={myProps} />. Has anyone worked with this technology before and knows if it's somehow possible to pass props to a custom node? :) Thanks!
You can do like this
const YourComponent = () => {
const componentNode = { reactComponent: (props) => <ComponentNode myProp="myProps" {...props} />};
return (
<ReactFlow
onLoad={onLoadTree}
elements={nodesAndEdges}
nodeTypes={componentNode}
/>
);
};
Here through props you get the predefined props like data, id etc.
You can pass any data you want in the data property section when creating a new node :
const newNode = {
...nodeProps,
data: {
myCustomProps: "test",
},
}
It seems you can pass props in data property of node.
Hello from December 2022:
withProps HOC:
import { createElement } from 'react';
const withProps = (WrappedComponent, additionalProps = {}) => {
return (props) => {
return createElement(WrappedComponent, {
...props,
...additionalProps,
});
};
};
export default withProps;
Component:
const myNodeTypes= React.useMemo(
() => ({
myNode: withProps(myNode, { myProp }),
}), []
);
<ReactFlow
nodeTypes={myNodeTypes}
>
</ReactFlow>
, Using props I was able to effectively pass state upwards from my child component to its parent, but a change in the state does not cause a re-render of the page.
import React, { useState } from "react";
export default function App() {
const AddToList = (item) => {
setText([...text, item]);
};
const removeFromList = (item) => {
const index = text.indexOf(item);
setText(text.splice(index, 1));
};
const [text, setText] = React.useState(["default", "default1", "default2"]);
return (
<div className="App">
<div>
<button
onClick={() => {
AddToList("hello");
}}
>
Add
</button>
</div>
{text.map((item) => {
return <ChildComponent text={item} removeText={removeFromList} />;
})}
</div>
);
}
const ChildComponent = ({ text, removeText }) => {
return (
<div>
<p>{text}</p>
<button
onClick={() => {
removeText(text);
}}
>
Remove
</button>
</div>
);
};
In the snippet, each time AddToList is called, a new child component is created and the page is re-rendered reflecting that. However, when i call removeFromList on the child component, nothing happens. The page stays the same, even though I thought this would reduce the number of childComponents present on the page. This is the problem I'm facing.
Updated Answer (Following Edits To Original Question)
In light of your edits, the problem is that you are mutating and passing the original array in state back into itself-- React sees that it is receiving a reference to the same object, and does not update. Instead, spread text into a new duplicate array, splice the duplicate array, and pass that into setText:
const removeFromList = (item) => {
const index = text.indexOf(item);
const dupeArray = [...text];
dupeArray.splice(index, 1);
setText(dupeArray);
};
You can see this working in this fiddle
Original Answer
The reason React has things like state hooks is that you leverage them in order to plug into and trigger the React lifecycle. Your problem does not actually have anything to do with a child attempting to update state at a parent. It is that while your AddToList function is properly leveraging React state management:
const AddToList = (item) => {
setText([...text, item]);
};
Your removeFromList function does not use any state hooks:
const removeFromList = (item) => {
const index = text.indexOf(item);
text.splice(index, 1); // you never actually setText...
};
...so React has no idea that state has updated. You should rewrite it as something like:
const removeFromList = (item) => {
const index = text.indexOf(item);
const newList = text.splice(index, 1);
setText(newList);
};
(Also, for what it is worth, you are being a little loose with styles-- your AddToList is all caps using PascalCase while removeFromCase is using camelCase. Typically in JS we reserve PascalCase for classes, and in React we also might leverage it for components and services; we generally would not use it for a method or a variable.)
I'm trying to call a vanilla javascript class inside a component.
This vanilla class is a distributor of cards and i wanted to separate the cards distribution logic from the component.
Where should I instanciate my vanilla class ?
How sure am I of the integrity of this instance (like when the components update) ?
I tried some things like putting in it inside useEffect(()=>{},[]) when the components mount but it didn't work (i didn't have access to my instance), I found this way but it works partially :
import * as WebBrowser from 'expo-web-browser';
import React, { useState, useEffect } from 'react';
import {
Image,
Platform,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
Button,
} from 'react-native';
import BasicMonoSlide from '../components/BasicMonoSlide';
import BasicMonoSlideDistributor from '../models/BasicMonoSlideDistributor';
export default function GameScreen(props) {
const [currentSlide, setCurrentSlide] = useState({});
const [joueurs,setJoueurs] = useState(
JSON.parse(JSON.stringify(props.joueurs))
);
var basicMonoSlideDistributor = new BasicMonoSlideDistributor();
useEffect(()=>{
console.log(currentSlide);
setCurrentSlide(getBasicMonoSlide());
},[]);
useEffect(()=>{
console.log(joueurs);
},[joueurs])
const nextSlide = () => {
console.log("appel next slide");
setCurrentSlide(getBasicMonoSlide());
};
const getBasicMonoSlide = ()=>{
console.log("appel getBasicMonoSlide");
var newSlideData = basicMonoSlideDistributor.getNewSlideData(joueurs,modifyJoueurs,()=>{nextSlide();});
console.log(newSlideData[2]);
return {type:'basicMonoSlide',slide:<BasicMonoSlide questionText={newSlideData[0]} btnText={newSlideData[1]} btnClickHandler={newSlideData[2]}/>};
};
const modifyJoueurs = (index,nom,sexe,orientation,enCoupleAvec,score) => {
var joueursActuel = joueurs;
console.log("modif du joueur a l'index "+index+" "+nom+","+sexe+","+orientation+","+enCoupleAvec+","+score);
const newJoueursArray = joueurs.map((item, indexMap) => {
if (indexMap === index) {
item.index=index;
item.nom=nom;
item.sexe = sexe;
item.orientation=orientation;
item.enCoupleAvec=enCoupleAvec;
item.score=score;
return item;
} else {
return item;
}
});
setJoueurs(newJoueursArray);
}
return (
<View style={styles.container}>
{currentSlide.slide||null}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
});
My "newSlideData[2]", the onPress given to my slide that has a button is an arrow function defined in my class BasicMonoSlideDistributor calling an arrow function defined in my component and when i click it works 1 or 2 times then does nothing
I know it's long, maybe just answer the first questions :)
probably the slide is attached to the DOM but you don´t see this because you have problems with the CSS, check if in the element is add to the DOM (with the dev tools in your browser) and if is attached, search the problem in the CSS.