I'm working on integrating a third-party video recording library into my React application. This third-party package integrates directly into a DOM <video> element and has it's own state in such a way that state updates in my component must not cause a re-render of the <video> tag, as that will break the integration. I tried a naive approach like this:
{this.videoNode.current || <video ref={this.videoNode} playsInline className="video-js vjs-default-skin"></video>}
But it only causes React to complain with this error when a re-render was triggered:
Error: Objects are not valid as a React child (found: [object HTMLVideoElement]). If you meant to render a collection of children, use an array instead.
Is there a clean and simple way to do this without having to instantiate that <video> node in the raw HTML outside React?
React reuses the existing dom elements where it can, so as long as you avoid some things that might force a remount you shouldn't need to do any trickery and just render a <video> tag normally.
Things that can cause problems:
1) React assumes that components with different types are a new subtree. So if you did something like this, then the change from div to span would cause the video element to remount:
const Example = () => {
const [withDiv, setWithDiv] = useState(true);
useEffect(() => {
setTimeout(() => setWithDiv(false), 5000);
}, []);
if (withDiv) {
return (
<div><video/></div>
);
} else {
return (
<span><video/></div>
);
}
}
2) key is a special property that can be used to inform react that two components are or are not the same component from one render to another. If the key changes, the component will remount, even if they might otherwise look the same. So the following case will cause the video player to remount:
const Example = () => {
const [key, setKey] = useState(1);
useEffect(() => {
setTimeout(() => setKey(2), 5000);
}, []);
return (
<div key={key}><video/></div>
)
}
But for most other cases, this shouldn't be a problem. The first time you render, a <video> element will be added to the dom, and then on subsequent renders that element will be reused.
For more information, see react's documentation on reconciliation
Related
A lot of things are causing my React functional components to flicker as new data comes in from various fetches and everything is constantly redrawn top-to-bottom.
For example, if I have a top-level App component which somewhere down the line includes a MyForm component, and the App does an initial fetch of DB Lookups which I depend on in the MyForm (passed as a prop, lookups={lookups}), the MyForm will flicker as it redraws itself on receiving the new prop values.
App
useEffect(() => {
fetchLookups();
}, []);
const fetchLookups = async () => {
const url = '/api/general/lookups';
try {
const responseLookups = await axios.get(url);
setLookups(responseLookups.data);
}
}
MyForm - will flicker - note also I have to check for the Non-NULL Fetch result from App, which is a pain
<Form.Control
value={props.lookups &&
props.lookups.filter(item => item.id === 30)
.map((item, index) => {
return (
item.description
);
})
/>
Also, in the MyForm component, there are lots of use inits/listeners with useEffect(.., []) and they will also cause the form to re-render. I have no way of knowing what the order will be on all those inits/listeners.
So my questions are, what's the best way to deal with this flickering?
Can I detect when a functional component is fully rendered, to show a loading spinner covering the full initialization sequence?
Can I block re-renders in any way?
I thought react-spring useSpring() causes the component to re-render a lot, so if it is a component that already has a lot of CPU intensively work to do, then react-spring is not best suited for the task.
I can see that it re-renders a lot, in their example:
https://codesandbox.io/s/musing-dew-9tfi9?file=/src/App.tsx
(by looking at the console.log output, which has a lot of print out of renderCount. The print out is a lot more when we change the duration to 5000 which is 5 seconds).
Likewise, if it is a component that is similar to react-spring, it'd render a lot:
https://codesandbox.io/s/wonderful-currying-9pheq
However, the following code:
let renderCount = 0
export default function App() {
const styles = useSpring({
loop: true,
to: [
{ opacity: 1, color: '#ffaaee' },
{ opacity: 0.5, color: 'rgb(14,26,19)' },
{ transform: 'translateX(100px)' },
{ transform: 'translateX(0px)' },
],
from: { opacity: 0, color: 'red', transform: 'translateX(0px)' },
config: { duration: 500 },
})
console.log('renderCount', ++renderCount)
return <a.div style={styles}>I will fade in and out</a.div>
}
Demo: https://codesandbox.io/s/dazzling-rgb-j2bx3?file=/src/App.tsx
We can see that the renderCount hardly get printed out at all. react-spring should need to keep on updating the style of the component, so after a minute, I'd expect a lot of print out of renderCount like the first two examples above, but it does not.
How and why doesn't react-spring cause a lot of re-rendering in this case, and how do we know in what situation would react-spring cause a lot of re-rendering (and how to prevent it)?
react-spring updates styles incrementally to create animations (as opposed to css animations with transition).
Naive animations outside React
If react-spring was to exist outside of React (which it OBVIOUSLY doesn't because then it wouldn't be named react-spring), this could most easily be done by modifying a given element's style by means of Javascript according to some predetermined pattern based on multiple factors (like delay, duration, etc....). One scenario could be
...
setTimeout(() => document.getElementById("#el").style.opacity = 0.34,100)
setTimeout(() => document.getElementById("#el").style.opacity = 0.39,150)
setTimeout(() => document.getElementById("#el").style.opacity = 0.42,200)
...
setTimeout(() => document.getElementById("#el").style.opacity = 1.0, 1000)
Exactly how this would be implemented is of course not the point of this answer and the above would be a very naive implementation, but this is basically what could go on if we wanted to make some animated transition where the interpolation between two endpoints would be calculated and implemented by ourselves (using spring physics) as opposed to in the browser (with css transition).
Naive animations in React
In React, we know that the preferred way to do things is to provide changes inside React, which React then processes after which necessary changes to the DOM is handled by React. Taking the previous (naive) example to React, this would imply some scheme where a state storing the opacity would be updated repeatedly until the desired endpoint was reached.
const Component = () => {
...
const [opacity, setOpacity] = useState(0)
useEffect(() => {
...
setTimeout(() => setOpacity(0.34),100)
setTimeout(() => setOpacity(0.39),150)
setTimeout(() => setOpacity(0.42),200)
...
setTimeout(() => setOpacity(1.0), 1000)
}, [])
return (
<div style={{ opacity }} ... />
)
}
This would work, but as one would expect, it could be quite burdensome since animations are supposed to happen fast and smooth and React rerendering on every animation frame could be problematic; if the component within which animation took place was expensive to render, the animation itself could be suffering and not look very good.
react-spring in React
The solution to this problem by react-spring is to do updates OUTSIDE of React via refs instead. The previous toy example could look like:
const Component = () => {
...
const ref = useRef(null)
useEffect(() => {
if(ref.current) {
...
setTimeout(() => ref.current.style.opacity = 0.34,100)
setTimeout(() => ref.current.style.opacity = 0.39,150)
setTimeout(() => ref.current.style.opacity = 0.42,200)
...
setTimeout(() => ref.current.style.opacity = 1.0, 1000)
}
}, [])
...
return (
<div ref={ref} ... />
)
}
Again, this is an example, exactly how one would implement this in the best way (as in react-spring) is a different story. But we can agree on that if we would log to the console every time the above component rendered, it would only log once even though the opacity would continue to change.
To sum up, when react-spring is used optimally, it uses refs to update properties on DOM elements whereupon React is by-passed. Thanks to this, a component may render only once but still make repeated animations. This particularly applies to the situation when the api is used to perform updates (as oppose to storing a state in a parent component which is set every time we want an animation to take place):
const [spring, api] = useSpring(() => ({ <INITIAL PROPS> })) // use api to make changes
const spring = useSpring({ <INITIAL PROPS }) // rerender component to update props
When using the basic HTML elements supplied by react-spring (such as animated.div, animated.span etc...), react-spring takes care of attaching a ref on the corresponding DOM element and via this ref, it manages to animate the element and therefore also all the content in it. When creating your own custom component wrapped in animated, it is your concern to make sure that your custom component can take a ref (via forwardRef) and to pass it on to the element which should be animated, if you want optimal animations. If you don't do this, the element will be rerendered on every animation frame by react-spring. Even though this works too, it is suboptimal from a performance point of view.
Your examples
In your examples some other things are at play as well. In the first example, the hook useMeasure is being used from react-use-measure. This hook will continuously provide different values from the child component (here height is provided) whereupon the parent will rerender. Since Tree components are nested, whenever ONE Tree component changes height, all parent Tree components whose heights will be changed will also rerender. We therefore see quite a lot of rerendering. Also, because StrictMode is enabled, the number is doubled. useSpring is used without api but it doesn't matter here since the parent rerenders a lot due to useMeasure anyways.
In your second example with react-spring, the api is not used either but since the animation is looped, it doesn't require any state in the parent to be set and so it doesn't rerender. Because the parent doesn't rerender, the animated component doesn't rerender either and so also in this case, it doesn't matter if we use the api or not. In this example if we would like to update the animated props however, using the api to do so would cause the parent NOT to rerender whereas otherwise, the state holding the props to update to would reside in the parent (or a grand-parent) and so the animated component would rerender as well when the parent rerenders.
I want to implement a specific mouse over behavior that can be added to any part of the app.
I want this to not create extra nested markup, if that can be avoided at all.
In Angular this can be achieved with something like <some-component myBehavior="…">, where I implement a myBehavior attribute directive.
In React with functional components, the mainstream way to achieve this appears to be with hooks, where I could associate a ref with the top-level element in my component and pass that ref to my hook (useMyBehavior(mainElRef, …)).
While it seems to make sense, unfortunately, I’d have to do this with each component that wants this behavior (which could be many). Additionally, if the top-level element is another custom component, getting the ref of the “concrete” underlying DOM element (div, span, etc.) could involve yet more logic.
All of that is doable, and is better than extra nested markup (which comes with many implications for existing styling), but it’s a lot of effort and extra logic I’d rather avoid if possible. After all, all I need my hook to be aware of is when mouse pointer enters current component’s element’s bounding box.
Hence the question: what’s the most feasible way of letting my hook smartly get a hold of the concrete DOM element that is the top-level wrapper for the current component without having to pass it a ref?
Unfortunately there's no shortcut to getting the ref to the top-level DOM element of a component. React Hooks don't "know" in which component they are rendered. And adding this functionality would break one of React's principles:
React components hide their implementation details, including their rendered output.
However, you can create and return a ref in your hook, so it doesn't need to be passed a ref.
function useMyBehavior() {
const ref = useRef();
useEffect(() => {
function onMouseover(e) {
// do whatever with ref.current
}
ref.current.addEventListener("mouseover", onMouseover);
return () => ref.current.removeEventListener("mouseover", onMouseover);
}, [ref.current]);
return ref;
}
function Component() {
const ref = useMyBehavior();
return (
<NestedComponent ref={ref} prop={"prop"} />
);
}
const NestedComponent = React.forwardRef((props, ref) => {
return (
<div ref={ref}>
{props.prop}
</div>
);
}
So long as custom components use React.forwardRef to forward the ref down to a DOM element, the hook will work. See the docs section on Forwarding Refs.
I have a component that dispatches some actions when a state somewhere else in the app changes.
But this is somehow giving me the dreaded change in the order of Hooks error.
In reading the rules of hooks it says basically wrapping hooks in conditionals is verboten... Which I am not currently doing.
However I am rendering lists of items and the values of those lists changes, as the server loads data. Using selectors and redux-sagas makes all of this pretty impossible to actually debug, so how else might I track down the root of the problem?
I have one component:
const PhraseList = props => {
const trainingPhrases = useSelector(selectFilteredPhrases());
return (
<div className={styles.Wrapper}>
<ul className={opStyles.listReset}>
{
trainingPhrases.map((phrase, idx) => {
return PhraseItem({ phrase, idx });
})
}
</ul>
</div>
);
which calls the PhraseItem component, which throws an Error.
const PhraseItem = ({ phrase, idx }) => {
// this line chokes
const [checked, setChecked] = useState(false);
return (
<li key={"tr-" + idx}>
<div className={styles.container}>
<div className={styles.phraseText}>{phrase.text}</div>
</div>
</li>
);
};
I'm debugging changes in the selector (selector factory) and that is what seems to trigger the app to crash.
export const selectFilteredPhrases = () =>
createSelector(trainingPhrases, phraseFilter, (trainingPhrases, phraseFilter) => {
console.log('trainingPhrases', trainingPhrases)
return trainingPhrases
});
but if data didn't change ever, it's hard to buidl an app :O
I saw an answer about using JSX which I don't think is relevant. And not much else on this issue.
redux, sagas, hooks, selectors... the react DSL is more and more layers and it's hard to debug deep in the framework.
UPDATE. I think this might be because I'm calling two sagas one after the other and this causes some kind of race condition in the UI?
const pickIntent = () => {
dispatch(pickIntentAction(intentId));
dispatch(getIntentPhrasesAction(intentId));
I think this answer is related, as it is already mentioned in this link, if you call the jsx component as a function react thinks that, hooks which are declared in this jsx function is part of the app component, so if your array trainingPhrases has fewer element on the first render than on the second one it will cause this warning (there will be less rendered jsx components, which means there will be less hooks). solution is simple call jsx component like this <PhraseItem phrase={phrase} idx={idx} />
I have a React component which receives children, wrapping them in a DOM element:
const MyComponent = ({ children }) => <div>{children}</div>;
When children resolves to 0 DOM nodes, I want to skip rendering the container DOM element (div in this example).
(This is because the container DOM element may contribute semantics or styles which we don't want/need if the element is going to be empty.)
E.g.
const MySubComponent = () => null;
declare const maybeChild: string | undefined;
const el = <MySubComponent />;
const el = (
<MyComponent>
{maybeChild}
{el}
</MyComponent>
)
In this example, the resulting DOM would be <div></div>.
Presumably I should do this by counting the number of DOM children inside MyComponent (using this as a condition for what to render), but there doesn't seem to be an easy way to do this. (The React.Children API refers to the children in the React node tree, rather than the DOM, so this doesn't seem to be helpful in this case.)
One solution I'm aware of is to ensure a 1:1 mapping of React nodes to DOM nodes. However this seems to be impractical because components often have rendering conditions inside of them.
This problem feels like it should be re-engineered. I know this is not a satisfactory solution, but children could contain literally hundreds of DOM nodes and could be intense in their depth. It does not feel prudent to embark on this. Also, there should be a general hesitation when exposing implementation details of a child to its parent. If you truly wanted to do this i would look towards React refs, but they're only available post initial render.
You should use React.Fragment and check for children.
e.g.
const MyComponent = ({ children }) => <React.Fragment>
{children ?
<div>{children}</div>
:
null
}
</React.Fragment>;
*I noticed that you use typescript and I'm don't know much about typescript and how to do checking, but if you follow the logic, I think you can work it out using typescript
Edit:
So what you need to do is check if the children is a valid React Element.
To check that you will need React.isValidElement
And here is how you can check if it's valid or not
const MyComponent = ({ children }) => {
let valid = true;
if (Array.isArray(children)) {
for (let i = 0; i < children.length; i++) {
if (!isValidElement(children[i])) {
valid = false;
}
}
} else {
valid = isValidElement(children);
}
if (valid) {
return <div>{children}</div>;
}
return null;
};
I created a codesandbox to check if works or not.
Using React.isValidElement you can do other checkings (maybe only rendering valid React Components)
And again, sorry for not giving the answer in typescript.
Edit:
If this solution is still not satisfatory, because of elements that can render null, then you should take a look at lluisrojass explanation.