ReactDom createPortal child components not working - javascript

I tried to create a dropdown menu with createPortal() because of visibility problems due to the stacking context, but the child components are not working as they should. The OperationButton has an onConfirm event.
createPortal(
<div
className={`widgetActionsMenu`}
style={{ position: 'alternative', top: '27px' }}>
{allowedOperations.map((pseudoOperation) => {
const [operation, disabled] = pseudoOperation.split(':');
return (
<OperationButton
disabled={!!disabled}
key={operation}
operation={operation}
itemId={props.itemId}
/>
);
})}
</div>
,
document.querySelector(`.flowChartActionLevel`)
)

Actuall assigning it to a ref solved the problem, it now finds the child elements and their properties.
const portalRef = React.useRef(null);

Related

React Toggle Body Class with button

I'm still learning React but I'm having an issue toggling a body class with a button in the menu.
const toggleSideMenu = event => {
// toggle class on click
//Below is not correct
event.getElementsByTagName('body').classList.toggle('sb-sidenav-toggled');
};`
<button onClick={toggleSideMenu} id="sidebarToggle" href="#!"><i className="fas fa-bars"></i></button>
I'm used to doing this easily in jQuery but it's not recommended to use jQuery in React because of the dom. I would appreciate any suggestions.
Thanks so much!
In this example, we are using the useState hook to keep track of the toggle state. The initial state is set to false. We are using the isToggled state in the JSX to determine what to render on the screen, and to update the text of the button.
We have an onClick event on the button, which calls the setIsToggled function and pass the negation of the current state (!isToggled), this is the way to toggle the state, every time the button is clicked.
import React, { useState } from 'react';
const MyComponent = () => {
// useState hook to keep track of the toggle state
const [isToggled, setIsToggled] = useState(false);
return (
<div>
{/* render some content or change className based on the toggle state */}
<p className={isToggled? "class1" : "classB">Toggled on</p>
<button onClick={() => setIsToggled(!isToggled)}>
{isToggled ? 'Turn off' : 'Turn on'}
</button>
</div>
);
}
export default MyComponent;
But if you need to do something more advanced, maybe you can learn more about React Context.
import React, { useState } from 'react';
// Create a context to share the toggle state
const ToggleContext = React.createContext();
const MyApp = () => {
// useState hook to keep track of the toggle state
const [isToggled, setIsToggled] = useState(false);
return (
<ToggleContext.Provider value={{ isToggled, setIsToggled }}>
<MyComponent1 />
<MyComponent2 />
{/* any other components that need access to the toggle state */}
</ToggleContext.Provider>
);
}
const MyComponent1 = () => {
// use the toggle state and toggle function from the context
const { isToggled, setIsToggled } = useContext(ToggleContext);
return (
<div>
<p className={isToggled? "class1" : "classB">Toggled on</p>
<button onClick={() => setIsToggled(!isToggled)}>
{isToggled ? 'Turn off' : 'Turn on'}
</button>
</div>
);
}
const MyComponent2 = () => {
// use the toggle state from the context
const { isToggled } = useContext(ToggleContext);
return (
<div>
{isToggled ? <p>Toggled on</p> : <p>Toggled off</p>}
</div>
);
}
export default MyApp;
A very basic example to show you how to use state to maintain whether a menu should be open or not.
It has one button that when clicked calls a function that updates the state.
It has one Menu component that accepts that state, and uses CSS to determine whether it should be "open" (ie on/off screen).
Like I said, as simple as I could make it.
const { useState } = React;
function Example() {
// The state set to either true or false
// Initially it's false / menu closed
const [ menuOpen, setMenuOpen ] = useState(false);
// When the button is clicked we take the
// previous state and toggle it - either from true
// to false, or vice versa
function handleClick() {
setMenuOpen(prev => !prev);
}
// One Menu component that accepts that state
// and one button that updates the state
return (
<div>
<Menu open={menuOpen} />
<button onClick={handleClick}>
Toggle Sidebar Menu
</button>
</div>
);
}
// Small menu (an aside element) which uses CSS
// to work out its position on the screen
// It does this by creating a classList using the default
// "menu" which it ties together with "open" but it only
// adds that if the state is true
// And then just use that joined array as the className on
// the element
// You can see in the CSS what both those classes do
function Menu({ open }) {
const menuStyle = [
'menu',
open && 'open'
].join(' ');
return (
<aside className={menuStyle}>
I am a sidebar
</aside>
);
}
ReactDOM.render(
<Example />,
document.getElementById('react')
);
.menu { width: 100px; top: 0px; left: -120px; background-color: salmon; position: fixed; height: 100vh; padding: 10px; transition-property: left; transition-duration: 0.25s;}
.open { left: 0px; }
button { position: fixed; left: 150px; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
getElementsByTagName() is method of Document or Element, not react event.
What you need to do, is to look for body inside document.
Also getElementsByTagName(), returns HTMLCollection (many elements), so you need to grab first one (usually there is only one body element on page)
document.getElementsByTagName('body')[0].classList.toggle('sb-sidenav-toggled');
There is also shortcut for body element document.body, so it can be also written as:
document.body.classList.toggle('sb-sidenav-toggled');

React component state not updating with passed props

I have created an Accordion component which has data(object) and expanded(boolean) as props.
expanded props is used to set the expanded/collapsed state of this component passed as a prop.
const DeltaAccordion = ({ index, data, expanded = true }) => {
Accordion component also has an internal state
const [isExpanded, setIsExpanded] = useState(expanded);
which is used for expanding/collapsing the accordion.
Below is my complete component
Accordion.jsx
import React, { useState } from "react";
// styles
import styles from "./index.module.scss";
const Accordion = ({ index, data, expanded = true }) => {
// state
const [isExpanded, setIsExpanded] = useState(expanded);
console.log(data.name, `prop-val==${expanded}`, `inner-state==${isExpanded}`);
return (
<div
className={`${styles.container} caption ${isExpanded && styles.expanded}`}
>
<div className={styles.header} onClick={() => setIsExpanded(!isExpanded)}>
<div>{data.name}</div>
<div>Click</div>
</div>
<div className={styles.content}>
{data.newValue && (
<div className={styles.newValue}>
<span>{data.newValue}</span>
</div>
)}
{data.oldValue && (
<div className={styles.oldValue}>
<span>{data.oldValue}</span>
</div>
)}
</div>
</div>
);
};
export default Accordion;
The parent component has a list of data and it loops through the list to create an accordion for each data item.
App.js file
{dataList.map((data, index) => (
<Accordion data={data} expanded={!collpaseAll} index={1} />
))}
Here goes problem
There is also a button in my App.js file which is for either expanding/collapsing all the accordions together.
But when I pass its value as prop expanded to the accordion component then this value is not getting applied to the internal isExpanded state of the accordion component.
Here is live running code at codesandbox: https://codesandbox.io/s/goofy-nobel-qfxm1?file=/src/App.js:635-745
Inside the Accordion
const [isExpanded, setIsExpanded] = useState(expanded);
This line will take first time(on first render) value. To assign it next time(rerender) value you need to add a effect
useEffect(() => {
setIsExpanded(expanded);
}, [expanded]);
And in your case, you can use the props expanded directly inside Accordion, you dont need to take it in local state.

How to access child component node from parent

I'm trying to implement a feature to close a menu component when clicking outside of it. To achieve this I check if current target is present in ref node. The problem is when I click on Icon component... Inspecting it, e.target happens to be img from Icon component, and if I search it on ref.current, it's not present... Is there a way to link parent and child nodes together to achieve this condition ref.current.contains(e.target) when i click on a child component?
Parent component:
function Menu ({showMenu, close}) {
const ref = useRef(null)
useEffect(() => {
document.addEventListener('click', handleClickOutside)
return () =>{
document.removeEventListener('click', handleClickOutside)
}
}, [])
function handleClickOutside(e) {
if (ref.current && !ref.current.contains(e.target) && showMenu) {
close()
}
}
return (
<div ref={ref}>
<Icon action={openMenu2}/>
<h1>Menu</h1>
</div>
)
}
Child
function Icon ({action}) {
return (
<div onClick={() => action()}>
<i>
<img src={imageSrc} alt="icon"></img>
</i>
</div>
)
}
if u just want to use child Component's ref , u can pass ref to props.action
//Child
const cRef = useRef(null)
function Icon ({action}) {
return (
<div ref= onClick={() => action(cRef)}>
<i>
<img src={imageSrc} alt="icon"></img>
</i>
</div>
)
}
then u can use it in parent Component
or u can move those logic to child component
To achieve this I check if current target is present in ref node. The problem is when I click on Icon component... Inspecting it, e.target happens to be img from Icon component, and if I search it on ref.current, it's not present...
You can pass menu toggle function directly to down children to children,
Toggle Function
With this approach you don't need to check if showMenu is true because
this function will close the menu if it is open, and it will open if it is close
Assuming your state is at the parent of Menu component
const toggleMenu = () => {
setMenuOpen(!menuOpen);
};
Then in Menu component pass menuOpen as menu's state and toggleMenu as function to change it.
function Menu({ toggleMenu, menuOpen }) {
return (
<div style={{ display: "flex", alignItems: "center" }}>
<Icon toggleMenu={toggleMenu} />
<h1>Menu : {`${menuOpen ? "open" : "closed"} in Menu component`}</h1>
</div>
);
}
And in Icon component you can toggle the menu
function Icon({ toggleMenu }) {
return (
<div onClick={toggleMenu}>
<img
style={{ width: 35, cursor: "pointer" }}
alt="hamburger-menu"
src="https://cdn4.iconfinder.com/data/icons/wirecons-free-vector-icons/32/menu-alt-512.png"
/>
</div>
);
}
export default Icon;
In action on codesandbox :https://codesandbox.io/s/eager-joliot-zb3t7?file=/src/Menu.jsx:54-299
If your app is getting complicated with passing states and passing state change functions I recommend you to check redux pattern.
Here link for redux : https://react-redux.js.org/

Is it safe to useMemo for JSX?

I'm trying to do a memoize Modal and I have a problem here.
When I change input I dont need to re-render the Modal component.
For example:
Modal.tsx looks like this:
import React from "react";
import { StyledModalContent, StyledModalWrapper, AbsoluteCenter } from "../../css";
interface ModalProps {
open: boolean;
onClose: () => void;
children: React.ReactNode
};
const ModalView: React.FC<ModalProps> = ({ open, onClose, children }) => {
console.log("modal rendered");
return (
<StyledModalWrapper style={{ textAlign: "center", display: open ? "block" : "none" }}>
<AbsoluteCenter>
<StyledModalContent>
<button
style={{
position: "absolute",
cursor: "pointer",
top: -10,
right: -10,
width: 40,
height: 40,
border: 'none',
boxShadow: '0 10px 10px 0 rgba(0, 0, 0, 0.07)',
backgroundColor: '#ffffff',
borderRadius: 20,
color: '#ba3c4d',
fontSize: 18
}}
onClick={onClose}
>
X
</button>
{open && children}
</StyledModalContent>
</AbsoluteCenter>
</StyledModalWrapper>
);
};
export default React.memo(ModalView);
Here is an example of how I wrap it.
import React from 'react'
import Modal from './modal';
const App: React.FC<any> = (props: any) => {
const [test, setTest] = React.useState("");
const [openCreateChannelDialog, setOpenCreateChannelDialog] = React.useState(false);
const hideCreateModalDialog = React.useCallback(() => {
setOpenCreateChannelDialog(false);
}, []);
return (
<>
<input type="text" value={test} onChange={(e) => setTest(e.target.value)} />
<button onClick={() => setOpenCreateChannelDialog(true)}>Create channel</button>
<Modal
open={openCreateChannelDialog}
onClose={hideCreateModalDialog}
children={<CreateChannel onClose={hideCreateModalDialog} />}
/>
</>
};
I know, Modal re-rendered because children reference created every time when App component re-renders (when I change an input text).
Know I'm interested, if I wrap <CreateChannel onClose={hideCreateModalDialog} /> inside React.useMemo() hook
For example:
const MemoizedCreateChannel = React.useMemo(() => {
return <CreateChannel onClose={hideCreateModalDialog} />
}, [hideCreateModalDialog]);
And change children props inside Modal
from:
children={<CreateChannel onClose={hideCreateModalDialog} />}
to
children={MemoizedCreateChannel}
It works fine, but is it safe? And it is only one solution that tried to memoize a Modal?
Memoizing JSX expressions is part of the official useMemo API:
const Parent = ({ a }) => useMemo(() => <Child1 a={a} />, [a]);
// This is perfectly fine; Child re-renders only, if `a` changes
useMemo memoizes individual children and computed values, given any dependencies. You can think of memo as a shortcut of useMemo for the whole component, that compares all props.
But memo has one flaw - it doesn't work with children:
const Modal = React.memo(ModalView);
// React.memo won't prevent any re-renders here
<Modal>
<CreateChannel />
</Modal>
children are part of the props. And React.createElement always creates a new immutable object reference (REPL). So each time memo compares props, it will determine that children reference has changed, if not a primitive.
To prevent this, you can either use useMemo in parent App to memoize children (which you already did). Or define a custom comparison function for memo, so Modal component now becomes responsible for performance optimization itself. react-fast-compare is a handy library to avoid boiler plate for areEqual.
Is it safe? Yes. At the end of the day the JSX is just converted into a JSON object, which is totally fine to memoize.
That said, I think it is stylistically a bit weird to do this, and I could foresee it leading to unexpected bugs in the future if things need to change and you don't think it through fully.

Is my entire React component re-rendering unnecessarily when the state changes?

I am trying to create an accordion component using React, but the animation is not working.
The basic idea is, I believe, pretty standard, I am giving each item body a max-height of 0 which is affected by adding a show class to an element. I am able to select and show the item I want, but the animation to slide in/out is not working.
With the Chrome dev tools open, when I click on one of the items I can see that the whole "accordion" element is flashing, which leads me to believe that the whole element is being re-rendered. But I am unsure why this would be the case.
Here is the relevant Accordion component:
import React, { useState } from "react";
const Accordion = ({ items }) => {
const [selectedItem, setSelectedItem] = useState(0);
const AccordionItem = ({ item, index }) => {
const isOpen = index === selectedItem;
return (
<div className="accordion-item">
<div
onClick={() => {
setSelectedItem(index);
}}
className="accordion-header"
>
<div>{item.heading}</div>
</div>
<div className={`accordion-body ${isOpen ? "show" : ""}`}>
<div className="accordion-content">{item.body}</div>
</div>
</div>
);
};
return (
<div className="accordion">
{items.map((item, i) => {
return <AccordionItem key={i} item={item} index={i} />;
})}
</div>
);
};
export default Accordion;
And here is a codepen illustrating the problem:
https://codesandbox.io/s/heuristic-heyrovsky-xgcbe
of course, its going to re-render. When ever you call setSelectedIem, state changes and hence react re-renders on state change to exhibit that change.
Now if you place this
const [selectedItem, setSelectedItem] = useState(0);
inside Accordion Item, it would just re-render accordion item, but would mess up your functionality.

Categories