I have a wrapper component in my App as follows. I have logic in the wrapper component that gets the DOM nodes with props.children that have a specific data attribute.
When the DOM elements are direct children, this works fine. When they are children of any nested component they're not found. How can I iterate through the entire structure and get all instances of the DOM nodes by attr?
I'm new to react and sure this should be straightforward, however I've not been able to implement from a number of examples / SO answers. I've tried to implement useRef and useContext hooks but can't get this?
// App.js
<Wrapper>
<div data-elem></div> // Get's found
<Component {...pageProps} />
</Wrapper>
// index.js
const Page = () => {
return (
<>
<div data-elem></div> // Not found
</>
);
}
// Wrapper.js ( simplified version )
export default function Wrapper(props) {
const detectedElements = props.children.filter((item) => item.props['data-elem'] === true);
console.log(detectedElements.length)
return (
<div className="wrapper">
{children}
</div>
)
}
Ultimately I managed to achieve this simply by using querySelectorAll within the useEffect hook. I'm not sure if this is the 'correct' way to do it, but is definitely the most straightforward of the the variations I tried, and seems to do the job, regardless of where the corresponding elements are.
useEffect(() => {
const detectedElements = document.querySelectorAll('[data-elem]')
}, []);
Related
I have two layouts one for mobile second for desktop view. Thier structure is different however both inject the same childrens. I would like to prevent childrens rerender when the layout switch. Here is pseudocode which reflect the case(or check live https://playcode.io/1193034):
import React, { useState } from "react";
interface ParentProps {
children: React.ReactNode;
}
const Parent = ({ children }: ParentProps): JSX.Element => {
const [value, setValue] = useState(false);
if (value) {
return (
<>
<button type="button" onClick={() => setValue((val) => !val)}>
Turn into false
</button>
{children}
</>
);
}
return (
// some additional elements for desktop view
<div>
<div>
<button type="button" onClick={() => setValue((val) => !val)}>
Turn into true
</button>
{children}
</div>
</div>
);
};
const ChildrenComponent = () => {
console.log("rerender Children...");
return <p>children component</p>;
};
export const App = ()=> {
return (
<Parent>
<ChildrenComponent />
</Parent>
);
}
What could I do?
So far I have tried to wrap children component in Rect.memo and useMemo (inside Parent) both didn't work. Maybe it's just impossible to do
You will not be able to avoid rerendering in your scenario, as you are rendering different elements in different positions
The way React knows what elements to rerender is by creating a tree structure (the virtual DOM) and comparing it to the browser DOM. When a node in the virtual DOM changes in relation to the browser DOM, it will be replaced (and therefore rerendered), along with every child element of this node. This process is called reconciliation.
Because of this, even if some of the child components are the same, once you change the position or the type of their parent elements, there is no way to avoid their rerendering.
On a sidenote, you could avoid the rerendering if you restructure your component to always return the same types of elements in the same positions, and make them responsive by using CSS media queries.
I am building a sequential step-based wizard flow that looks something like this:
export default function App() {
return (
<Wizard footer={<Footer />}>
<Step1 />
<Step2 />
</Wizard>
);
}
the <Step /> components are mounted/unmounted based on the step the user is currently on.
Problem:
I have an expensive component, <SomeExpensiveComponent />. It is very computationally expensive to mount, but I need to render it in multiple <Step />s. I want to avoid having it mount multiple times in my app:
// ❌ BAD - each step is mounting its own, new instance of SomeExpensiveComponent
const Step1 = () => {
return (
<Step.Wrapper>
<Step.Preview>
<SomeExpensiveComponent />
</Step.Preview>
<Step.Content>Step1</Step.Content>
</Step.Wrapper>
);
};
const Step2 = () => {
return (
<Step.Wrapper>
<Step.Preview>
<SomeExpensiveComponent />
</Step.Preview>
<Step.Content>Step2</Step.Content>
</Step.Wrapper>
);
};
I need to render this component inside of various <Step />s, but only want to mount the component one time, and share the single instance between steps. Something like this:
export default function App() {
return (
<>
{/* 👀 mount expensive component once, here */}
<SomeExpensiveComponent />
<Wizard footer={<Footer />}>
<Step1 />
<Step2 />
</Wizard>
</>
);
}
const Step1 = () => {
return (
<Step.Wrapper>
<Step.Preview>
{/* ✅ ...and output that single instance into an "outlet" when this component is mounted */}
<OutletForExpensiveComponent />
</Step.Preview>
<Step.Content>Step1</Step.Content>
</Step.Wrapper>
);
};
const Step2 = () => {
return (
<Step.Wrapper>
<Step.Preview>
{/* ✅ ...and output that single instance into an "outlet" when this component is mounted */}
<OutletForExpensiveComponent />
</Step.Preview>
<Step.Content>Step2</Step.Content>
</Step.Wrapper>
);
};
The hypothetical "outlet" above would simply dictate different render targets for the single shared instance of SomeExpensiveComponent.
At first, it sounds like React Portals might be the solution to this problem:
Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component.
However, it seems Portals are primarily focused on the opposite direction - getting a leaf node to render in a higher scope or different tree, not getting a trunk node to render output into a different leaf node.
Question:
Are portals the right way to accomplish the above? If so, how?
Is this accomplishable with React Context? If so, how?
Code / demo
You can see my current attempt (using React Context) here: https://codesandbox.io/s/quizzical-elgamal-wvhc3h?file=/src/App.tsx
Problems:
SomeExpensiveComponent renders > 1 time on app load.
SomeExpensiveComponent re-renders on navigate.
I tried this approach with react portal, which seems closer, but still get 2 renders instead of 1 on page load:
https://codesandbox.io/s/young-bash-iij752?file=/src/App.tsx
Suppose I have a React component without the capability of changing its source code. This component, lets say <Demo /> renders a lot of <a> ...<a/> HTML elements. Is it possible to add an attribute inside those elements programmatically and how?
you could use a wrapper where you create a reference for the wrapper tag. with that you could query for specific elements and change its attributes accordingly:
const wrapperComponent = props => {
const myRef = React.createRef()
useEffect(() => {
myRef.current.querySelector("a").innerText = "got changed!"
}, [myRef])
return (
<div ref={myRef}>
<Component {...props} />
</div>
)
}
I played with React for several years, still confused with mount/unmount mechanism in some case.
Since mount/unmount is the place to perform side effect, I do not want them to be invoked randomly. So I need to figure out how they work. As far as I can understand currently, when the virtual dom do not present in real dom, it tend to be unmounted. However, it seems not the whole story, and I can not reason it about
function TestMount(props) {
useEffect(() => {
console.log("componentDidMount", props.name);
return () => {
console.log("componentWillUnount", props.name);
};
}, []);
return <h1>Test content {" " + JSON.stringify(props.name)}</h1>;
}
function Update({ click }) {
return <button onClick={click}>Update</button>;
}
function App() {
const [count, setCount] = useState(0);
const Component = name => <TestMount name={name} />;
return (
<div className="App">
<h1>{count}</h1>
<Component name="one" />
{Component("two")}
<Update click={() => setCount(x => x + 1)} />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Component One is remount overtime the app render while Component two not?Why this happen?
Component is a new function each time App is rendered, so <Component name="one" /> is remounted each time too, they are considered different components.
The result of Component("two") call is <TestMount name={"two"} />, TestMount stays the same each time App is rendered, so it's not remounted.
Component is invalid component for what it's used for, to pass name string as name prop to TestMount component because name parameter is not a string but props object when Component is used like <Component name="one" />. name => <TestMount name={name} /> is render function, it's preferable to name it accordingly like renderTestMount for clarity because components aren't supposed to be called directly like Component("two").
In case a function is supposed be used as component or render function interchangeably, the signature should be changed to ({ name }) => <TestMount name={name} />.
The expected behaviour could be achieved for <Component name="one" /> by memoizing Component:
const Component = useCallback(({ name }) => <TestMount name={name} />, []);
But since Component doesn't depend on App scope, a correct way is to define it outside:
const Component = ({ name }) => <TestMount name={name} />;
function App() {...}
For instance, this is the reason React Router Route has separate component and render props for a component and render function. This allows to prevent unnecessary remounts for route components that need to be defined dynamically in current scope.
The key to such issue is the difference between the React Component and React element, put shortly React is smart with element not Component
Component vs element
Component is the template used to create element using <> operation. In my prospective, <> is pretty much like new operator in OOP world.
How React perform update between renders
Every time the render method(or functional component) is invoked. The new element is created using <>, however, React is smart enough to tell the element created between renders are actually the same one, i.e. it had been created before and can be reused as long as the element is created by the same Component
How about different Component
However when the identity of the Component using to generate element changes(Even if the Components look same), React believes something new come though, so it remove(unmount) the previous element and add(mount) the new one. Thus componentDidMount or componentWillUnmount is invoked.
How is confusing
Think we got a Component and when we generate element using <Component /> react can tell the same elements because they are generated by the same Component
However, HOCComponent=()=><Component />; element= <HOCComponent />, every time element is generated, it used a different Component. it is actually a HOC constructed dynamically. Because the HOC is created dynamically inside render function, it can be confusing on the first glance.
Is that true
I never found any offical document about the idea above.However the code below is enough to prove
function TestMount(props) {
useEffect(() => {
console.log("componentDidMount", props.name);
return () => {
console.log("componentWillUnount", props.name);
};
}, []);
return <h1>Test content {" " + JSON.stringify(props.name)}</h1>;
}
function Update({ click }) {
return <button onClick={click}>Update</button>;
}
let _Component;
function cacheComponent(C) {
if (C && !_Component) {
_Component = C;
}
return _Component || null;
}
const CacheComponent2 = once(({ name }) => <TestMount name={name} />, []);
function App() {
const [count, setCount] = useState(0);
// can be used as a HOC of TestMount or a plain function returnnung a react element
const Component = name => <TestMount name={name} />;
const CacheComponent1 = cacheComponent(Component);
const CacheComponent3 = useCallback(
({ name }) => <TestMount name={name} />,
[]
);
return (
<div className="App">
<h1>{count}</h1>
{/* used as HOC */}
<Component name="one" />
{/* used as function returnning the element */}
{Component("two")}
<CacheComponent1 name="three" />
<CacheComponent2 name="four" />
<CacheComponent3 name="five" />
<Update click={() => setCount(x => x + 1)} />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Also the code above provide three different ways to avoid the undesired mount/unmount. All the solutions are cache the identity of the HOC somehow
Take a look at this simple example:
const List = function({ loading, entity }) {
return (
<Layout loading={loading}>
<span>Name: {entity.name}</span>
</Layout>
);
};
Layout component is rendering its children only when loading is false. But the problem here is that React is resolving Layout children immediatelly. Since entity is null (while loading=true) I get error that it can't read name of null. Is there a simple way to avoid this error since this span will always be rendered when entity is not null?
Currently I know about 3 options:
Move this span to stateless function which receives entity as prop
Wrap whole children of Layout in function and then support function children in Layout
Just use {entity && <span>Name: {entity.name}</span>}
Why do I have to use one of these options and can I make React to consider those children as function and resolve block inside later on before the render?
I just stumbled upon the same problem.
What worked for me was passing children as a function:
ready: boolean;
children?: () => ReactNode,
}> = ({ ready, children }) => {
return (
<div>{
ready ?
children() :
(
<div>not ready</div>
)
}</div>
);
};
<Layout loading={loading}>
{() =>
<span>Name: {entity.name}</span>
}
</Layout>
Though it's still not perfect.