How do I render a react portal modal with different children? - javascript

Right now, I have a react portal rendering a 'pinnable' side drawer modal that will display based on a redux state. The contents of the modal will have information based on where that modal was pinned from, in this case my notifications.
The problem I'm running into at the moment is that since the modal will be pinnable in multiple places, I'm not exactly sure of the logic on how to handle the modal contents if the modal is already pinned.
I've tried/considered the following:
Just have one portal render its children dynamically. Unfortunately the location of where the portal will be rendered does not contain the contents and logic of the modal, so I believe this can't be done.
Compare props.children and if they're not identical, render the newer portal and deconstruct the other. I'm hesitant to use this approach since I believe there's a better solution out there.
Render the portals based on Ids and deconstruct/reconstruct where needed if one exists. I'm leaning towards this approach, but again I'd like to see if there's a better one.
Portal location:
export default function PaperContainer() {
return <div id="pinned-container"></div>;
}
Portal:
export default function PinnedContainer(props) {
const pinned = useSelector(state => state.ui.isDrawerPinned);
return (
pinned &&
createPortal(
<div>
<div>{props.children}</div>
</div>
,
document.getElementById('pinned-container')
)
);
}
Where the portals are called (simplified for brevity):
export default function PortalCallLocationOne() {
const dispatch = useDispatch();
const pinContainer = () => {
dispatch(toggleDrawer());
};
return (
<>
<Button startIcon={<Icon>push_pin</Icon>} onClick={() => pinContainer}>
Pin Notification
</Button>
<PinnedContainer>
//Notification
</PinnedContainer>
</>
);
}
export default function PortalCallLocationTwo() {
const dispatch = useDispatch();
const pinContainer = () => {
dispatch(toggleDrawer());
};
return (
<>
<Button startIcon={<Icon>push_pin</Icon>} onClick={() => pinContainer}>
Pin List
</Button>
<PinnedContainer>
// List
</PinnedContainer>
);
</>
}

I tried going off of #3 and destroying pinned-container's first child if it existed and replace it with the new children. This didn't work since React was expecting that child and kept throwing failed to execute removeChild on node errors.
Unfortunately it looks like react is unable to replace portal children instead of appending them.
However, I was able to solve my issue by unpinning the portal and repinning it with redux actions.
export default function PinnedContainer(props) {
const pinned = useSelector(state => state.ui.isDrawerPinned);
useEffect(() => {
if (pinned) {
dispatch(clearPinned());
dispatch(pinDrawer(true));
}
}, []);
return (
pinned &&
createPortal(
<div>
<div>{props.children}</div>
</div>
,
document.getElementById('pinned-container')
)
);
}
Reducer:
export const initialState = {
isDrawerPinned: false,
}
export const reducer = (state = initialState, action) => {
switch(action.type){
case actionTypes.PIN_DRAWER:
return {
...state,
isDrawerPinned: action.isPinned ? action.isPinned : !state.isDrawerPinned,
};
case actionTypes.CLEAR_PINNED:
return {
...state,
isDrawerPinned: state.isDrawerPinned ? !state.isDrawerPinned : state.isDrawerPinned
};
}
}

Related

Creating tab component in React and handling click events for child components

New to React and trying to build a tabular component. I know I'm reinventing the wheel but I'm trying to take this as a learning experience.
Here is how I intend to use the component:
<Tabs>
<Tabs.MenuItems>
<Tabs.MenuItem>Tab item 1</Tabs.MenuItem>
<Tabs.MenuItem>Tab item 2</Tabs.MenuItem>
<Tabs.MenuItem>Tab item 3</Tabs.MenuItem>
<Tabs.MenuItem>Tab item 4</Tabs.MenuItem>
</Tabs.MenuItems>
<Tabs.Panes>
<Tabs.Pane><div>placeholder></div></Tabs.Pane>
<Tabs.Pane><div>placeholder></div></Tabs.Pane>
<Tabs.Pane><div>placeholder></div></Tabs.Pane>
<Tabs.Pane><div>placeholder></div></Tabs.Pane>
</Tabs.Panes>
</Tabs>
My current implementation works in displaying the items properly. But the one challenge I am facing is being able to handle the onClick event for the Tabs.MenuItem. I understand that I should not be handling the onClick in the Tabs.MenuItem child component, and rather should be handled in the upmost parent Tabs component.
I tried using forwardedRef but that posed some limitations in accessing the props.children. Even if I managed to get it working syntactically, I am not even sure how the Tabs component is suppose to access that ref.
The idea here is that depending on what Tabs.MenuItem is in an active state, it will correspond to the same child Tabs.Pane component index to render that pane.
import React, { forwardRef, useState } from "react";
const Tabs = (props, { activePane }) => {
return (
props.children
);
}
const MenuItems = (props) => {
React.Children.forEach(props.children, child => {
console.log(child);
})
return (
<div className="ui secondary menu" style={props.style}>
{props.children}
</div>
)
}
const MenuItem = (props) => {
const [isActive, setActive] = useState(false);
return (
// eslint-disable-next-line jsx-a11y/anchor-is-valid
<a className={isActive ? "item active" : "item"} onClick={() => setActive(!isActive)}>{props.children}</a>
)
}
// const MenuItem = forwardRef((props, ref) => (
// // eslint-disable-next-line jsx-a11y/anchor-is-valid
// <a ref={ref} className="item">{props.children}</a> // error accessing props.children
// ))
const Panes = (props) => {
return (
props.children
)
}
const Pane = (props) => {
return (
props.children
)
}
Tabs.MenuItems = MenuItems;
Tabs.MenuItem = MenuItem;
Tabs.Panes = Panes;
Tabs.Pane = Pane;
export default Tabs;
I am not looking for someone to complete the entire tabular functionality, just an example of how I can forward the children references to the topmost parent so that I can handle click events properly.

How to save a component state after re-rendering? React js

There are some movie cards that clients can click on them and their color changes to gray with a blur effect, meaning that the movie is selected.
At the same time, the movie id is transferred to an array list. In the search bar, you can search for your favorite movie but the thing is after you type something in the input area the movie cards that were gray loses their style (I suppose because they are deleted and rendered again based on my code) but the array part works well and they are still in the array list.
How can I preserve their style?
Search Page:
export default function Index(data) {
const info = data.data.body.result;
const [selectedList, setSelectedList] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
return (
<>
<main className={parentstyle.main_container}>
<NavBar />
<div className={style.searchbar_container}>
<CustomSearch
onChange={(e) => {
setSearchTerm(e.target.value);
}}
/>
</div>
<div className={style.card_container}>
{info
.filter((value) => {
if (searchTerm === '') {
return value;
} else if (
value.name
.toLocaleLowerCase()
.includes(searchTerm.toLocaleLowerCase())
) {
return value;
}
})
.map((value, key) => {
return (
<MovieCard
movieName={value.name}
key={key}
movieId={value._id}
selected={selectedList}
setSelected={setSelectedList}
isSelected={false}
/>
);
})}
</div>
<div>
<h3 className={style.test}>{selectedList}</h3>
</div>
</main>
Movie Cards Component:
export default function Index({ selected, movieName, movieId, setSelected }) {
const [isActive, setActive] = useState(false);
const toggleClass = () => {
setActive(!isActive);
};
useEffect(()=>{
})
const pushToSelected = (e) => {
if (selected.includes(e.target.id)) {
selected.splice(selected.indexOf(e.target.id), 1);
console.log(selected);
} else {
selected.push(e.target.id);
console.log(selected);
console.log(e.target);
}
setSelected([...selected]);
toggleClass();
};
return (
<div>
<img
className={isActive ? style.movie_selected : style.movie}
id={movieId}
name={movieName}
src={`images/movies/${movieName}.jpg`}
alt={movieName}
onClick={pushToSelected}
/>
<h3 className={style.title}>{movieName}</h3>
</div>
);
}
I can't directly test your code so I will assume that this is the issue:
Don't directly transform a state (splice/push) - always create a clone or something.
Make the setActive based on the list and not dependent. (this is the real issue why the style gets removed)
try this:
const pushToSelected = (e) => {
if (selected.includes(e.target.id)) {
// filter out the id
setSelected(selected.filter(s => s !== e.target.id));
return;
}
// add the id
setSelected([...selected, e.target.id]);
};
// you may use useMemo here. up to you.
const isActive = selected.includes(movieId);
return (
<div>
<img
className={isActive ? style.movie_selected : style.movie}
id={movieId}
name={movieName}
src={`images/movies/${movieName}.jpg`}
alt={movieName}
onClick={pushToSelected}
/>
<h3 className={style.title}>{movieName}</h3>
</div>
);
This is a very broad topic. The best thing you can do is look up "React state management".
As with everything in the react ecosystem it can be handled by various different libraries.
But as of the latest versions of React, you can first start by checking out the built-in tools:
Check out the state lifecycle: https://reactjs.org/docs/state-and-lifecycle.html
(I see in your example you are using useState hooks, but I am adding these for more structured explanation for whoever needs it)
Then you might want to look at state-related hooks such as useState: https://reactjs.org/docs/hooks-state.html
useEffect (to go with useState):
https://reactjs.org/docs/hooks-effect.html
And useContext:
https://reactjs.org/docs/hooks-reference.html#usecontext
And for things outside of the built-in toolset, there are many popular state management libraries that also work with React with the most popular being: Redux, React-query, Mobx, Recoil, Flux, Hook-state. Please keep in mind that what you should use is dependant on your use case and needs. These can also help you out to persist your state not only between re-renders but also between refreshes of your app. More and more libraries pop up every day.
This is an ok article with a bit more info:
https://dev.to/workshub/state-management-battle-in-react-2021-hooks-redux-and-recoil-2am0#:~:text=State%20management%20is%20simply%20a,you%20can%20read%20and%20write.&text=When%20a%20user%20performs%20an,occur%20in%20the%20component's%20state.

How to re-render React components without actually changing state

In my React application I have a component called Value, which has several instances on multiple levels of the DOM tree. Its value can be shown or hidden, and by clicking on it, it shows up or gets hidden (like flipping a card).
I would like to make 2 buttons, "Show all" and "Hide all", which would make all these instances of the Value component to show up or get hidden. I created these buttons in a component (called Cases) which is a parent of each of the instances of the Value component. It has a state called mode, and clicking the buttons sets it to "showAll" or "hideAll". I use React Context to provide this chosen mode to the Value component.
My problem: after I click the "Hide All" button and then make some Value instances visible by clicking on them, I'm not able to hide all of them again. I guess it is because the Value components won't re-render, because even though the setMode("hideAll") function is called, it doesn't actually change the value of the state.
Is there a way I can make the Value instances re-render after calling the setMode function, even though no actual change was made?
I'm relatively new to React and web-development, I'm not sure if it is the right approach, so I'd also be happy to get some advices about what a better solution would be.
Here are the code for my components:
const ModeContext = React.createContext()
export default function Cases() {
const [mode, setMode] = useState("hideAll")
return (
<>
<div>
<button onClick={() => setMode("showAll")}>Show all answers</button>
<button onClick={() => setMode("hideAll")}>Hide all answers</button>
</div>
<ModeContext.Provider value={mode}>
<div>
{cases.map( item => <Case key={item.name} {...item}/> ) }
</div>
</ModeContext.Provider>
</>
)
}
export default function Value(props) {
const mode = useContext(ModeContext)
const [hidden, setHidden] = useState(mode === "showAll" ? false : true)
useEffect(() => {
if (mode === "showAll") setHidden(false)
else if (mode === "hideAll") setHidden(true)
}, [mode])
return (
hidden
? <span className="hiddenValue" onClick={() => setHidden(!hidden)}></span>
: <span className="value" onClick={() => setHidden(!hidden)}>{props.children}</span>
)
}
You first need to create your context before you can use it as a provider or user.
So make sure to add this to the top of the file.
const ModeContext = React.createContext('hideAll')
As it stands, since ModeContext isn't created, mode in your Value component should be undefined and never change.
If your components are on separate files, make sure to also export ModeContext and import it in the other component.
Example
Here's one way to organize everything and keep it simple.
// cases.js
const ModeContext = React.createContext('hideAll')
export default function Cases() {
const [mode, setMode] = useState("hideAll")
return (
<>
<div>
<button onClick={() => setMode("showAll")}>Show all answers</button>
<button onClick={() => setMode("hideAll")}>Hide all answers</button>
</div>
<ModeContext.Provider value={mode}>
<div>
{cases.map( item => <Case key={item.name} {...item}/> ) }
</div>
</ModeContext.Provider>
</>
)
}
export function useModeContext() {
return useContext(ModeContext)
}
// value.js
import { useModeContext } from './cases.js'
export default function Value(props) {
const mode = useContext(ModeContext)
const [hidden, setHidden] = useState(mode === "showAll" ? false : true)
useEffect(() => {
if (mode === "showAll") setHidden(false)
else if (mode === "hideAll") setHidden(true)
}, [mode])
return (
hidden
? <span className="hiddenValue" onClick={() => setHidden(!hidden)}></span>
: <span className="value" onClick={() => setHidden(!hidden)}>{props.children}</span>
)
}
P.S. I've made this mistake many times, too.
You shouldn't use a new state in the Value component. Your components should have an [only single of truth][1], in your case is mode. In your context, you should provide also a function to hide the components, you can call setHidden
Change the Value component like the following:
export default function Value(props) {
const { mode, setHidden } = useContext(ModeContext)
if(mode === "showAll") {
return <span className="hiddenValue" onClick={() => setHidden("hideAll")}></span>
} else if(mode === "hideAll") {
return <span className="value" onClick={() => setHidden("showAll")}>{props.children}</span>
} else {
return null;
}
)
}
P.S. Because mode seems a boolean value, you can switch between true and false.
[1]: https://reactjs.org/docs/lifting-state-up.html
There are a few ways to handle this scenario.
Move the state in the parent component. Track all visible states in the parent component like this:
const [visible, setVisibilty] = useState(cases.map(() => true))
...
<button onClick={() => setVisibilty(casses.map(() => false)}>Hide all answers</button>
...
{cases.map((item, index) => <Case key={item.name} visible={visible[index]} {...item}/> ) }
Reset the mode after it reset all states:
const [mode, setMode] = useState("hideAll")
useEffect(() => {
setMode("")
}, [mode])

React: override internal components with custom component

I have a modal that is completely self contained. The modal is opened via going to the modal route and all the functionality to close the modal from button or outside clicks is within the modal component. Basically the modal is not controlled by any parent passing state. I was given a task of making the modals button customizable, meaning passing in a new button component, so we can add the modal to our lib instead of copy pasting the code in projects. Lol this seemed simple enough, and maybe it is and I am just overthinking this.
I cant paste the actual code but I can use a contrived example. This is a very simplified version of the modal, keeping in mind it opens via route so there's really no state and setState in the actual code. Also here is a fiddle
const ModalHeader = ({ onClose }) => {
return (
<div className="modal__header">
<button
className="modal__close-btn"
data-testid="modal-close-button"
onClick={onClose}
/>
</div>
);
};
const Modal = ({ children }) => {
const [state, setState] = React.useState(true);
const handleCloseOutsideClick = () => {
setState(false);
};
const handleCloseButtonClick = () => {
setState(false);
};
const renderModal = () => {
return (
<div className="modal-overlay" onClick={handleCloseOutsideClick}>
<div className="modal">
<ModalHeader onClose={handleCloseButtonClick} />
{children}
</div>
</div>
);
};
return state ? renderModal() : null;
};
const App = () => {
return (
<Modal>
<div>Modal Children</div>
</Modal>
);
};
ReactDOM.render(<App />, document.querySelector('#app'));
I tried a few things, initially I attempted to find a way to pass in a new header component containing a button. Then as I got into the code I realized what I was doing would lose the self contained functionality of the modal. My approach was along the lines of below but obviously the onClick would be an issue since invoking the close functionality is internal.
So I tried using cloneElement to add props within the component if the custom header was detected:
// inside modal component
React.useEffect(() => {
React.Children.map(children, (child: React.ReactElement) => {
if (child && child.type === ModalHeader) {
setHederFound(true);
}
});
}, []);
// inside modal render:
<div className={modalClasses} onClick={stopPropagation}>
{!headerFound ? (
<ModalDefaultHeader onClose={handleCloseButtonClick} />
) : (
React.Children.map(children, (child: React.ReactElement) => {
if (child && child.type === ModalHeader) {
return React.cloneElement(child, {
onClose: handleCloseButtonClick,
});
}
})
)}
{children}
</div>;
Obviously that did not work because there's no onClick in the custom button. Anyways I am thinking that I am over complicating this. I just need a way to pass in a custom button while leaving the functionality internal to the modal. Any assistance would be appreciated.
Thanks in advance.

What is going wrong when I pass a boolean from a child to its parent component using React Hooks?

I'm currently trying to revise the dropdown menu component on my Gatsby site so that it reports a boolean to its parent component, a navbar. I plan on using that boolean to trigger some conditional CSS in Emotion.
The boolean isOpen reports if the dropdown menu is open or not, so true means it's open, and false means it's not.
As of now, I'm using React Hooks to pass that data from the child to the parent component. It seems like I'm successfully passing data, but when I click the dropdown menu, it sends both a true and a false boolean value in rapid succession, even as the menu remains open.
How do I revise this code so that isOpen in the child component is correctly reported to the parent component?
import React, { useState, useEffect } from "react"
const Child = ({ isExpanded }) => {
const [expandState, setExpandState] = useState(false)
useEffect(() => {
setExpandState(isOpen)
isExpanded(expandState)
})
return(
<dropdownWrapper>
<button
{...isExpanded}
/>
{isOpen && (
<Menu>
//menu items go here
</Menu>
)}
</dropdownWrapper>
)
}
const Parent = () => {
const [expandState, setExpandState] = useState(false)
const onExpand = (checkExpand) => {
setExpandState(checkExpand)
}
return(
<Dropdown
isExpanded={onExpand}
onClick={console.log(expandState)}
/>
)
}
Figured this one out myself. Parent needed a useEffect to register the incoming boolean.
Fixed code for the parent:
const Parent = () => {
const [expandState, setExpandState] = useState(false)
const onExpand = (checkExpand) => {
setExpandState(checkExpand)
}
useEffect(() => {
onExpand(expandState)
})
return(
<Dropdown
isExpanded={onExpand}
onClick={console.log(expandState)}
/>
)
}

Categories

Resources