Conditional Rendering in Styled Component based on Parents' Props - reactjs

How would I go about applying conditional styles to a Styled Component based on its parent component's props?
A high-level sample setup of my current code is below:
// File 1: GrandParent.js
import React, { useState } from 'react';
import Parent from './parent';
export default function GrandParent() {
const [ isExpanded, setIsExpanded ] = useState(false);
render (
<>
<Parent isExpanded={isExpanded} />
...other Stuff
</>
);
}
// File 2: Parent.js
import React from 'react';
import styled from 'styled-components';
import mediaQueries from './settings';
export default function Parent(props) {
render (
<div className={props.isExpanded ? `expanded` : ``}>
... other stuff
<Child>I'm a child.</Child>
</>
);
}
const Child = styled.div`
background-color: "red";
.expanded & {
background-color: "blue";
${mediaQueries.desktop`
background-color: "navy";
`}
}
`;
Essentially, what I'd like is for my <Child> styled component to have awareness of it's <Parent>'s props so that I can style it conditionally. Currently, I am just conditionally applying a class name to a div just inside the <Parent>, and then I'm using that class name to apply styling to the <Child>. But doesn't feel like a very Styled Components or React way of doing things.
What I'd like to be able to do is something more like this:
const Child = styled.div`
background-color: ${props => props.isExpanded ? 'blue' : 'red'};
`;
But is there a way for me to do this without having to pass in 'isExpanded' as a prop on the <Child>? In my actual code, there are many more children of <Parent> who will also need to know the status of 'isExpanded' for styling purposes, and I really don't want to have to pass that prop to all of them individually.
I think Context API might be the key here, but I'm already using a context several levels higher up above GrandParent in the React tree for global style settings, and I'm afraid that if I try to nest a 2nd context (perhaps wrapping it around <Parent> to let <Parent> and all of its children know about the status of 'isExpanded'), that then both <Parent> and <Child> might lose access to the outer context... or is that not how context nesting works?
I just know that I still need both <Parent> and <Child> to have access to some theme settings from the outer context. And I don't know if it's overkill to introduce a 2nd context at the <Parent> level just for keeping track of the status of that single 'isExpanded' prop.
The other requirement here is that I need <Child> to be able to handle media queries within its conditional styling as well (eg. only applying certain styles if 'isExpanded' is true and if it's a desktop viewport).
What's the proper way to set this up?

You are correct. Context is the way to go.
Context is designed to be a lightweight cross-component data store and using 2 contexts in this case is definitely not overkill; In fact, this use case is almost exactly what Context was created for. (Anecdotally, at work we use dozens of nested contexts).
export const ExpandedContext = createContext();
export default function GrandParent() {
const [ isExpanded, setIsExpanded ] = useState(false);
render (
<ExpandedContext.Provider value={{ isExpanded }}>
<Parent />
...other Stuff
<ExpandedContext.Provider/>
);
}
Unfortunately, there is no way to access context inside of a styled-component, so you'll have to pass in the value via props instead.
export default function Parent(props) {
const { isExpanded } = useContext(ExpandedContext);
render (
<div>
... other stuff
<Child isExpanded={isExpanded}>I'm a child.</Child>
</div>
);
}

You could make the parent's container div a styled component and refer to Child. Something like:
export default function Parent(props) {
render (
<ParentContainer isExpanded={props.isExpanded}>
... other stuff
<Child>I'm a child.</Child>
</ParentContainer>
);
}
const Child = styled.div`
// ...
`;
const ParentContainer = styled.div`
> ${Child} {
background-color: ${props => props.isExpanded ? 'blue' : 'red'};
}
`;

Related

emotion use css prop with custom components

In my application I have a component that I want to style with the css prop from outside.
function Component({css}:{css?: React.CSSProperties}) {
// some stuff going on here
return (
<div
css={{
color: blue,
...css
}}
>
// some stuff going on here
</div>
)
}
The background is as follows:
I want to use Component in different scenarios where I have to style the container based on the surrounding layout. E.g. flex, grid or in combination with some components I have to add different margins.
Now instead of introducing many props for all possible scenarios, I want to be able to style the container from outside the component.
E.g. usages of the component could be:
function Layout() {
return (
// some other components
<Component css={{margin: 12}}/>
// some other components
)
}
or
import {css} from "#emotion/react"
const style = css({margin: 12})
function Layout() {
return (
// some other components
<Component css={style}/>
// some other components
)
}
or
import {css} from "#emotion/react"
const style1 = css({margin: 12})
const style2 = css({color: 'red'})
function Layout() {
return (
// some other components
<Component css={[style1, style2]}/>
// some other components
)
}
I have the following problems:
If I use css as the prop name (as in the above example) the style is not applied. If I change the name of the prop to e.g. newCss it works as expected
React.CSSProperties is not the right prop type to handle all the possibilities of emotions css prop.
How can I merge the different css prop possibilities (object, list) with the css prop from Component?
In fact, we don't need to use the extra props. As Ben Laniado mentioned, the official documentation states
Any component or element that accepts a className prop can also use the css prop.
https://emotion.sh/docs/css-prop#use-the-css-prop
So what we need is accepting className and css as props and add className to the component. (We don't need css to the component but need it for types)
type ComponentProps = {
css?: SerializedStyles;
className?: string;
};
const Component: VFC<ComponentProps> = ({ className }) => {
return (
<div css={{ color: blue }} className={className}>
hello world
</div>
);
};
export default function App() {
return (
<div className="App">
<Component css={{ margin: 12 }} />
</div>
);
}
This is the full working example.
https://codesandbox.io/s/react-component-accepting-emotion-css-prop-wskbh?file=/src/App.tsx
The right way of achieving this functionality is modifying the component to accept extra props. This way the css prop passed into the component would be merged with the one within the component.
function Component({prop1, prop2, propN, ...props}) {
// some stuff going on here
return (
<div
css={{
color: blue,
}}
{...props}
>
// some stuff going on here
</div>
)
}
Now you can use additional styles on your component and it will be rendered properly.
function Layout() {
return (
// some other components
<Component css={{marginTop: "1em"}}/>
// some other components
)
}
The side effect of this solution that any additional prop would be passed directly to the HTML element inside the component that takes {...props}.

React - styled components constructor from variable

Is there a way to pass the return of another variable to the styled constructor "styled(xx)"?
I get an error: Objects are not valid as a React child (found: object with keys {$$typeof, render, propTypes}).
import styled from 'styled-components/macro';
import { Link as LinkR } from 'react-router-dom';
import { Link as LinkS } from 'react-scroll';
const Link = props => (props.scroll ? LinkS : LinkR);
export const BtnContainer = styled(Link)`
margin: 16px 0;
padding: 16px 22px;
`
If I wrap the imported Link objects into `` or '' in the Link variable, it only outputs the return to the DOM. Ok, I expected that. :) But it shows me that something can be passed on to the constructor.
Wrapping in {} or $ {} do not work either.
Have you tried returning the rendered link components?
const Link = (props) => props.scroll ? <LinkS {...props} /> : <LinkR {...props} />;
This makes Link an actual component and styled-components can handle them

How to focus trap with styled-components? How to access classname from styled-components?

I'm currently using styled-components 5.0.1 with React. https://styled-components.com/
For context, my goal is to focus trap within a modal.
My problem is that classnames are randomly generated from styled-components so I'm not able to access these DOM nodes with querySelector. My other problem is that I'm not able to use React ref forwarding since I would have to do a lot of ref forwarding between component trees.
Is there a way to access the classname that is generated from styled-components? If so, I can use querySelector and go about the usual way of focus trapping by accessing the DOM nodes through querySelector.
Firstly, your component should be able to process the passed-in css:
import styled from "styled-components";
const Box = styled.div`
color: red;
${props => props.addCSS}
`;
const DatePicker = () => <Box>DatePicker</Box>;
Secondly, declare the css style.
import { css } from "styled-components";
const myCSS = css`
font-size: 60px;
`;
Lastly, pass it down to the child component.
<DatePicker addCSS={myCSS} />
I was able to solve this issue by accessing the DOM nodes with using a data- attribute.
For example,
const myComponent = styled.div`
// ...styles here
`;
const foo = () => (
<myComponent
data-foo-bar="foobar"
/>
);
console.log(document.querySelector('[data-a11y-id="HeaderNavBar-SearchButton"]');
// returns the DOM element for myComponent

Locate a component in React project

I wrote a Logout button component in my React app for which I wish to locate at the top right corner of the screen.
render() {
<LogoutButtonComponent height: , backgroudColor: />
}
It wouldn't let me assign any values for height and etc.
This is the Logout component:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export default class LogOutButton extends Component {
static contextTypes = {
store: PropTypes.object.isRequired,
};
handleClick = () => {
this.props.onLogout();
};
render() {
return <button type="button" onClick={this.handleClick}>Logout</button>;
}
}
Should I locate it by < col /> ?
To add inline styles, you should defined a style object as prop, and pass it values, like doniyor2109 mentioned. There are a few caveats to using this, however.
style={{ height: 100, height: '100px', height: '100%', minHeight: '100px'}}.
Not every value should be passed as integer, some need to be passed as a string
Not every css attribute gets passed as you would expect them to, the css min-height actually gets passed as minHeight, so replace all hyphens with lower camel case style
Inline styles get insanely difficult to manage. I suggest you at the very least create an object outside the component, and pass it in like:
const DivStyle = { minHeight: '100px' }
and then:
<LogoutButtonComponent style={DivStyle} />
You can prefix that DivStyle with an export if you want to import {DivStyle} from './somefile' in other places
I suggest you check out a library like styled-components as it makes styling much easier!
I suggest you check out this article which outlines your options
You don't really add styles to your component like that. It's better to add those styles in the source for the actual component. So how exactly do you want it displayed? I will provide a template kind of and you can change it to what you want.
Go to your source for your Logout Button Component. In the return of your render method try adding a div call it container. Then add styling in a css file to that div or if you are using react-bootstrap or reactstrap or #material/ui/core you can adjust the style according to their documentation.
You can add your css for the className .container to make it appear the way you would like.
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export default class LogOutButton extends Component {
static contextTypes = {
store: PropTypes.object.isRequired,
};
handleClick = () => {
this.props.onLogout();
};
render() {
return (
<div className="container">
{* notice the className here *}
<button type="button" onClick={this.handleClick}>Logout</button>
</div>
)
}
}
Hope this helps.

Material-UI #next Does not support props-based styling?

With the arrival of Material-UI#next comes the replacement of LESS-based styling with CSS-to-JS-based styling. However, the component demos on Material-UI's website appear to ignore the creation of props-based-styling. I'm trying to create divs containing various Material-UI components at specific absolute heights on my page, however, the requirement of the stylesheet being predefined outside of the class means that the properties within the stylesheet can't be based on props passed to the component. Am I missing something?
For example:
import React, {Component} from 'react';
import {withStyles, createStyleSheet} from 'material-ui/styles';
import Button from 'material-ui/Button';
const styleSheet = createStyleSheet({
container: {
position: "absolute",
top: /*How can this be dependent upon the props passed to the component?*/,
height: /*How can this be dependent upon the props passed to the component?*/,
}
});
class Foo extends Component {
render() {
let classes = this.props.classes;
return (
<div className={classes.container}>
<Button raised/>
</div>
);
}
}
export default withStyles(styleSheet)(Foo);
The example component displayed above can't have props-dependent styles, because props is not defined when the stylesheet is created. So how do I solve this problem? IS there a solution?
I would strongly advise you check out Styled Compoments. They make styling very simple and even allow you to pass third party components (in your case Material UI components). They also allow you to pass in props like the following:
const Stylesheet = styled.div`
color: ${props => props.primary ? 'white' :
`
<Stylesheet primary>My Div</Stylesheet>
Check out the docs for more details, that was a very simple example, but they are super easy to work with and you can accomplish exactly what you need with them.

Resources