Create React Portal via Ref - reactjs

I would like to create a Portal component that is supposed to be attached to it's container component, but not via the container's ID but by it's ref. In other words, I don't want to pass document.getElementById('CONTAINER_ID') as the second argument to the ReactDOM.createPortal function but to rely solely on the ref of the container being passed by React.forwardRef.
Is there a straightforward method of achieving this ?
Else maybe I would need to create a dom node "attached" to the ref first and than pass the node to the
createPortal function as the second argument ? I would like to avoid assigning the ids as much as possible. Here is the example code (I work in TypeScript) :
export default React.forwardRef<HTMLDivElement, Props>( (Props, ref) =>{
const {state, options, dispatch} = Props
if(!state.open) return null
return ReactDOM.createPortal(
<div
className={css`
height: 140px;
background: white;
overflow-y: scroll;
position: absolute;
width:100%;
`}
>
{options}
</div>,
ref.current // <-- THIS DOESN'T WORK
)
}
)

forwardRef is used when the parent component needs to get access to the child's element, not the other way around. To pass an element down to a child you'll do it through a prop. Also, since refs aren't populated until after the first render, you may need to render the parent component twice, once to put the container on the page, and then again to pass it to the child so the child can create a portal.
const Parent = () => {
const ref = useRef<HTMLDivElement>(null);
const [element, setElement] = useState<HTMLDivElement | null>(null);
useEffect(() => {
// Force a rerender, so it can be passed to the child.
// If this causes an unwanted flicker, use useLayoutEffect instead
setElement(ref.current);
}, []);
return (
<div ref={ref}>
{element && (
<Child element={element} {/* insert other props here */} />
)}
</div>
)
}
const Child = (props: Props) => {
const { state, options, dispatch, element } = props;
if (!state.open) {
return null;
}
return ReactDOM.createPortal(
<div
className={css`
height: 140px;
background: white;
overflow-y: scroll;
position: absolute;
width: 100%;
`}
>
{options}
</div>,
element
);
}

Related

React styled component not passing props from attribute

I'm trying (unsuccessfully) to pass a single color value from a parent to a child using styled-components.
If the color is passed correctly, the background should be the color that is passed. Else green.
No matter what I do, the background-color is green
What am I missing here?
The parent:
function App() {
return (
<div>
<Article color="red"/>
</div>
);
}
The child:
const MainContetnt = styled.div`
background-color: ${props => props.color ?? "green"};
flex: 1;
`;
const Article = (props) => {
return (
<MainContetnt>
Main content
</MainContetnt>
)
};
Props will not be passed automatically to a styled component, you still have to do it the usual way:
const Article = (props) => {
return (
<MainContetnt color="props.color">
Main content
</MainContetnt>
)
};

How to set state in useEffect hook using React and Typescript?

I want to change right property from 16px to 40px when user is in page "/items/:itemId" using React and Typescript.
Below is my component snippet,
const root = () => {
<PopupContextProvider>
<App/>
</PopupContextProvider>
}
export const PopupContextProvider = ({ children }: any) => {
return (
<popupContext.Provider value={context}>
{children}
{(condition1 || condition2) && (
<Popup onHide={dismiss} />
)}
</popupContext.Provider>
);
}
export function Popup({ onHide }: Props) {
return (
<Dialog>
<DialogBody>
<span>Title</span>
<Description/>
</DialogBody>
<Actions>
<span>Hide</span>
</Actions>
</Dialog>
);
}
const Dialog = styled.div`
position: fixed;
right: 16px;//want to change this to 40px if user is in page
"/items/:itemId"
display: flex;
flex-direction: column;
`;
What I have tried?
export function Popup({ onHide }: Props) {
const location = useLocation();
const [isView, setIsView] = React.useState(false);
if (location.pathname === '/items/:itemId') {
setIsView(true);
//Here, it doesn't change to true.
//How can I do the same in useEffect or something that updates
}
return (
<Dialog isView={isView}>
<DialogBody>
<span>Title</span>
<Description/>
</DialogBody>
<Actions>
<span>Hide</span>
</Actions>
</Dialog>
);
}
const Dialog = styled.div<isView?:boolean>`
position: fixed;
${({ isView }) => isView && 'right: 40px;'}
display: flex;
flex-direction: column;
`;
My above code doesn't update the position of the popup with right 40px even though user is in page "/items/:itemId".
I am not sure what is going wrong. Can someone help me with this? Thanks.
EDIT:
what i have tried based on one of the answer provided.
export function Popup({ onHide }: Props) {
const location = useLocation();
const [isView, setIsView] = React.useState(false);
React.useEffect(() => {
const match = matchPath(
location.pathname,
'/items/:itemId'
);
if (match) { //it doesnt get it into this condition since match is
//null
setIsScheduleView(true);
}
}, []);
return (
<Dialog isView={isView}>
<DialogBody>
<span>Title</span>
<Description/>
</DialogBody>
<Actions>
<span>Hide</span>
</Actions>
</Dialog>
);
}
You can use useEffect hook to implement the side effect that you want.
First, you can put "location" variable as one of the dependencies inside useEffect hook as below.
useEffect(() => {
// do your if condition
// set isView state as side effect
},[location]);
Secondly, I believe your condition is not correct.
location.pathname will not equal to '/items/:itemId' since :itemId is a dynamic parameter. So, you have an option to use includes method to check if it is your url or you can add a wrapper which will resolve to "/items" first , then to "/items/:itemId". When the route comes into the wrapper, you can do the styling.
EDIT: or better yet you can use matchPath api of react-router as #ludwiguer has mentioned also to match your path.
As explained by #Kevin Moe Myint Myat in his answer. Your condition is not correct as it contains a dynamic id in it. So you can check for /items using includes method and if this.props.match is available, then you can use it to check if param itemId is present or not. Like this.props.match.params.itemId, you can use this with previous condition with AND & operator in useEffect.
I imagine you are using react router, if so you can use matchPath https://reactrouter.com/web/api/matchPath
import { matchPath } from 'react-router';
setIsView(
!!matchPath(
this.props.location.pathname,
{path: 'items/:itemId'}
));

how to pass multiple classNames to inner children with emotion js

I want to split the components into baseUI one and styled one:
eg.
MyComponent.jsx
export default class MyComponent extends React.Component {
...
...
render() {
const { wrapperClassName, className, childClassName } = this.props;
return (
<div className={wrapperClassName>
<div className={className />
<div className={childClassName} />
</div>
)
}
}
StyledMyComponent.jsx
import styled from 'react-emotion'
const StyledMyComponent = styled(MyComponent)(
...
...
)
export default StyledMyComponent
however anything I put to the styled function's argument they will go to the className only, is there a way I specify which props goes to which className?
also can I do something like sass/less with children selector?
hypothetically something like this:
const classes = css`
color: red;
span { // this works
color: black;
}
.childClassName { // this doesn't work
color: green;
}
`
<MyComponent className={classes} />
No you can't.
What you can do, is create specific components for the underlying div. This is how I make my components:
const MyComponentStyle = styled('div')....;
const MySecondComponentStyle = styled('div')...;
const MyThirdStyle = styled('div')...;
const MyComponent = ({ wrapperClassName, childClassName, className }) =>
<MyComponentStyle className={wrapperClassName}>
<MySecondComponentStyle className={className} />
<MyThirdStyle className={childClassName} />
</MyComponentStyle>
)
}
}
Conditionally styling the element and its children based on class names
You can conditionally change the styling of stuff below the main component based on its classes.
Taking your example:
const Something = () => (
<MyComponent className={classes}>
<div className="childClassName">child</div>
<div className="otherChildClassName">child</div>
</MyComponent>
You can style the children like so:
const classes = css`
color: red;
span {
color: black;
}
& .childClassName {
color: green;
}
`
note the & character. It essentially means "this class". So & .childClassName means "childrens of this element with class childClassName.
You could also use &.someClassName (note the lack of space), which would mean: "this element when it also has a class named someClassName.

How to extend styled component without passing props to underlying DOM element?

I have a styled component that is extending a third-party component:
import Resizable from 're-resizable';
...
const ResizableSC = styled(Resizable)``;
export const StyledPaneContainer = ResizableSC.extend`
flex: 0 0 ${(props) => props.someProp}px;
`;
const PaneContainer = ({ children, someProp }) => (
<StyledPaneContainer
someProp={someProp}
>
{children}
</StyledPaneContainer>
);
export default PaneContainer;
This resulted in the following error in the browser console:
Warning: React does not recognize the someProp prop on a DOM
element. If you intentionally want it to appear in the DOM as a custom
attribute, spell it as lowercase someProp instead. If you
accidentally passed it from a parent component, remove it from the DOM
element
So, I changed the prop to have a data-* attribute name:
import Resizable from 're-resizable';
...
const ResizableSC = styled(Resizable)``;
export const StyledPaneContainer = ResizableSC.extend`
flex: 0 0 ${(props) => props['data-s']}px;
`;
const PaneContainer = ({ children, someProp }) => (
<StyledPaneContainer
data-s={someProp}
>
{children}
</StyledPaneContainer>
);
export default PaneContainer;
This works, but I was wondering if there was a native way to use props in the styled component without them being passed down to the DOM element (?)
2020 UPDATE: Use transient props
With the release 5.1.0 you can use transient props. This way you do not need an extra wrapper i.e. unnecessary code is reduced:
Transient props are a new pattern to pass props that are explicitly consumed only by styled components and are not meant to be passed down to deeper component layers. Here's how you use them:
const Comp = styled.div`
color: ${props => props.$fg || 'black'};
`;
render(<Comp $fg="red">I'm red!</Comp>);
Note the dollar sign ($) prefix on the prop; this marks it as transient and styled-components knows not to add it to the rendered DOM element or pass it further down the component hierarchy.
The new answer should be:
styled component:
const ResizableSC = styled(Resizable)``;
export const StyledPaneContainer = ResizableSC.extend`
flex: 0 0 ${(props) => props.$someProp}px;
`;
parent component:
const PaneContainer = ({ children, someProp }) => (
<StyledPaneContainer
$someProp={someProp} // '$' signals the transient prop
>
{children}
</StyledPaneContainer>
);
As suggested by vdanchenkov on this styled-components github issue you can destructure the props and only pass the relevant ones to Resizable
const ResizableSC = styled(({ someProp, ...rest }) => <Resizable {...rest} />)`
flex: 0 0 ${(props) => props.someProp}px;
`;
For those (like me) that landed here because of an issue with SC and react-router's Link.
This is basically the same answer as #tskjetne, but with JS syntax style.
const StyledLink = styled(({ isCurrent, ...rest }) => <Link {...rest} />)(
({ isCurrent }) => ({
borderBottomColor: isCurrent ? 'green' : 'transparent',
}),
)
Starting with version 5.1, you can use shouldForwardProp configuration property:
This is a more dynamic, granular filtering mechanism than transient props. It's handy in situations where multiple higher-order components are being composed together and happen to share the same prop name. shouldForwardProp works much like the predicate callback of Array.filter. A prop that fails the test isn't passed down to underlying components, just like a transient prop.
Keep in mind that, as in this example, other chainable methods should always be executed after .withConfig.
I find it much cleaner than transient props, because you don't have to rename a property, and you become explicit about your intentions:
const ResizableSC = styled(Resizable).withConfig({
// Filter out the props to not be shown in DOM.
shouldForwardProp: (prop, defaultValidatorFn) =>
prop !== 'someProp'
&& defaultValidatorFn(prop),
})`
flex: 0 0 ${(props) => props.someProp}px;
`;
This is especially handy if you are using TypeScript and sharing the same props type both in your main component and the corresponding styled component:
import { HTMLAttributes } from 'react';
import styled from 'styled-components';
// Props type.
type CaptionProps = HTMLAttributes<HTMLParagraphElement> & {
size: number,
};
// Main component.
export const CaptionStyles = styled('p').withConfig<CaptionProps>({
// Filter out the props to not be shown in DOM.
shouldForwardProp: (prop, defaultValidatorFn) => (
prop !== 'size'
&& defaultValidatorFn(prop)
),
})`
flex: 0 0 ${(props) => props.size}px;
`;
// Corresponding styled component.
export function Caption({ size }: CaptionProps) {
return (
<CaptionStyles size={size} />
);
}
shouldForwardProp API reference
You can try with defaultProps:
import Resizable from 're-resizable';
import PropTypes from 'prop-types';
...
const ResizableSC = styled(Resizable)``;
export const StyledPaneContainer = ResizableSC.extend`
flex: 0 0 ${(props) => props.someProp}px;
`;
StyledPaneContainer.defaultProps = { someProp: 1 }
const PaneContainer = ({ children }) => (
<StyledPaneContainer>
{children}
</StyledPaneContainer>
);
export default PaneContainer;
We can also pass props using 'attrs'. This helps in attaching additional props (Example taken from styled components official doc):
const Input = styled.input.attrs({
// we can define static props
type: 'password',
// or we can define dynamic ones
margin: props => props.size || '1em',
padding: props => props.size || '1em'
})`
color: palevioletred;
font-size: 1em;
border: 2px solid palevioletred;
border-radius: 3px;
/* here we use the dynamically computed props */
margin: ${props => props.margin};
padding: ${props => props.padding};
`;
render(
<div>
<Input placeholder="A small text input" size="1em" />
<br />
<Input placeholder="A bigger text input" size="2em" />
</div>
);

How to use the styled-component property innerRef with a React stateless component?

I have the following styled component:
const Component = styled.div`
...
`;
const Button = (props) => {
return (
<Component>
...
</Component>
);
};
export default styled(Button)``;
I want to get a reference to the underlying div of Component. When I do the following I get null:
import Button from './Button.js';
class Foo extends React.Component {
getRef = () => {
console.log(this.btn);
}
render() {
return (
<Button innerRef={elem => this.btn = elem} />
);
}
}
Any ideas why I am getting null and any suggestions on how to access the underlying div?
Note: The reason I am doing this export default styled(Button)``; is so that the export styled component can be easily extended.
I managed to accomplish this by passing a function down as a prop to the styled-component that I was targeting, then passing the ref back as an argument of the function:
const Component = styled.div`
...
`;
const Button = (props) => {
return (
<Component innerRef={elem => props.getRef(elem)}>
...
</Component>
);
};
export default styled(Button)``;
...
import Button from './Button.js';
class Foo extends React.Component {
getRef = (ref) => {
console.log(ref);
}
render() {
return (
<Button getRef={this.getRef} />
);
}
}
Passing a ref prop to a styled component will give you an instance of the StyledComponent wrapper, but not to the underlying DOM node. This is due to how refs work. So it's not possible to call DOM methods, like focus, on styled components wrappers directly.
Thus to get a ref to the actual, wrapped inner DOM node, callback is passed to the innerRef prop as shown in example below to focus the 'input' element wrapped inside Styled component 'Input' on hover.
const Input = styled.input`
padding: 0.5em;
margin: 0.5em;
color: palevioletred;
background: papayawhip;
border: none;
border-radius: 3px;
`;
class Form extends React.Component {
render() {
return (
<Input
placeholder="Hover here..."
innerRef={x => { this.input = x }}
onMouseEnter={() => this.input.focus()}
/>
);
}
}
render(
<Form />
);
NOTE:-
String refs not supported (i.e. innerRef="node"), since they're already deprecated in React.

Resources