Styled-component warning suggesting I use `attrs` method even though I am? - reactjs

I had a styled component that was rendering 3 times, each having a parallax effect.
const ParallaxContainer = styled.section`
position: absolute;
left: 0;
right: 0;
height: 100%;
opacity: 0.3;
bottom: ${props => props.bottomValue}px;
`;
The parallax is achieved by updating the bottom value every scrollEvent through a pretty expensive calculation. Meaning, this component is getting re-rendered very often.
Unsurprisingly, I was getting the warning Over 200 classes were generated for component styled.section. Consider using the attrs method, together with a style object for frequently changed styles. So I tried to follow the advice, and refactored the component to this:
const ParallaxContainer = styled.section.attrs(
({ bottomValue }) => ({
style: {
bottom: bottomValue + "px"
}
})
)`
position: absolute;
left: 0;
right: 0;
height: 100%;
opacity: 0.3;
`;
But I am still getting the same error. What am I doing wrong?
Sandbox demonstrating my issue: https://codesandbox.io/embed/styled-components-is-yelling-at-me-attrs-zhnof

The problem is the lines you left commented out in your style. Remember, your style is just a template string. The CSS-style commented lines are only ignored after the string has been interpolated and passed to Styled Components.
The Over 200 classes error happened in the first place because the style string needed to be re-computed on every scroll event, which results in a completely new styled component instance. Moving it to attrs is like defining the style in JS in the old-fashioned React way, so these styles don't go through Styled Components.

Related

Can I store a ref in Redux store, or is it considered bad practice?

The context:
Short explanation why I'm even asking this. I have a React App with Redux.
I have a component that shows a List of Chat Rooms. I also have a Appbar from Material UI with a position of sticky. Also the list of chatRooms is sticky so it sticks to the left side, without scrolling with the list of chats in the middle.
This won't work ofc because the list of chatRooms will hide behind the AppBar or better said, overlap with it. So I need to set the top value for chatRooms according to the AppBar's height.
The question:
My idea was to add useDimensions hook and measure the height of the AppBar. I need to pass a ref to the appbar. And then set the top value of chatRooms equal to this height. But I can't pass this directly to the component. So my question is:
Can I store the ref in redux store, or is it considered bad practice? If so how to solve this problem?
Don't store a ref in your redux store, there are better ways to fix this problem. I have a layout similar than yours and it works with css only. You should avoid sticky because of this overlap problem and use a css grid instead.
Here is my css, as you can see there are no sticky, but just a full page grid:
/* Main grid container (2 columns, 2 rows) */
#root {
height: 100%;
display: grid;
grid-template-columns: 220px 1fr;
grid-template-rows: 56px 1fr;
}
/* Top App bar, height=56px */
header {
grid-column: span 2;
}
/* List of chatRooms, width=220px */
aside {
overflow-x: hidden;
overflow-y: auto;
}
/* List of chats */
main {
overflow-y: auto;
}
If you want this grid to fill the whole page, then you need to add:
html { height: 100% }
body { height: 100% }

React + Three.js canvas does not resize correctly when making the browser window smaller

I am using react and three.js to display some 3D models in the browser. Everything works fine as long as the three.js component is the only element with visible content on the page. The three.js canvas will resize bigger and smaller just fine. You can wrap it in other containers and it still works fine. The issue starts when another element or component is added as a sibling.
I want to have a viewport with a fixed width sidebar. I'm doing this with a flexbox row container, wrapped around a sidebar component (a simple div with min-width set), and the responsive three.js component.
Resizing the window bigger works fine, the canvas fills the browser window appropriately. However, when resizing the window smaller, the canvas does not properly re-calculate it's size in relation to the available space left over by the static width sidebar, resulting in a canvas that does not fully fit in the browser and introducing scrollbars.
The canvas does shrink some, but not enough to keep it fully in the browser window. The same issue occurs vertically if a component sits above or below the three.js component. If you recompile the app while the browser window is smaller, the refreshed view will have the canvas properly resized.
Following the advice of many of the answers here on StackOverflow, the three.js react code looks something like this:
export class Viewport extends React.Component {
componentDidMount() {
const width = this.mount.clientWidth;
const height = this.mount.clientHeight;
window.addEventListener("resize", this.handleWindowResize);
// setup scene
this.scene = new THREE.Scene();
//setup camera
this.camera = new THREE.PerspectiveCamera( 75, width / height, 0.1, 1000 );
this.camera.position.set( 0, 5, 10 );
// setup rendering
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setClearColor('#666666');
this.renderer.setSize(width, height, false);
this.mount.appendChild(this.renderer.domElement);
// setup geo
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: '#433F81' });
this.cube = new THREE.Mesh(geometry, material);
this.scene.add(this.cube);
...
}
componentWillUnmount() {
window.removeEventListener("resize", this.handleWindowResize);
this.mount.removeChild(this.renderer.domElement);
}
handleWindowResize = () => {
const width = this.mount.clientWidth;
const height = this.mount.clientHeight;
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
this.renderer.setSize(width, height, false);
};
render() {
return (
<div className="viewport" style={{display: 'flex', width: "100%", height: "100%"}} ref={(mount) => { this.mount = mount }} />
);
}
}
The styling is simple css flexbox, with the elements set to full width and height (100%), with the sidebar having a set min-width.
.flex-row-container {
display: flex;
flex-flow: row;
height: 100%;
width: 100%;
}
.sidebar {
display: block;
min-width: 250px;
background-color: var(--color-mid-gray);
margin: 3px 4px 3px 4px;
box-sizing: border-box;
border: 1px solid var(--color-mid-gray);
outline: 3px solid var(--color-mid-gray);
}
What am I missing?
So after commenting, I think I got it working how you want.
Basically,
I removed setting display:flex on the viewport, as this is a child of an element (the flex-row-container div) with display:flex, it does not need it, but does need flex:1 1 auto to set how it will display within the flexbox (which I did in the css rather than the JSX).
The problem then became that the viewport div would expand, but not contract (because it contains the canvas), so adding overflow:hidden fixed that.
After making those changes, this.mount.clientWidth would get the desired result that could then be used to set the three canvas size.
See This Fiddle.
I added rendering and animation to the example so the result could be easily seen.
Hope this helps.
You might be getting incorrect values by using element.clientWidth. Have you tried using element.getBoundingClientRect() instead?
const rect = this.mount.getBoundingClientRect();
const width = rect.width;
const height = rect.height;

Problems with ref focus using componentDidUpdate + opacity and visibility animation in css

I'm trying to focus() an input inside a modal right after it opens using the modal's componentDidUpdate().
This is the CSS I'm using for the modal root element:
.auth-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
visibility: hidden;
opacity: 0;
transition: visibility 400ms, opacity 400ms;
&.auth-modal_is-opened {
visibility: visible;
opacity: 1;
}
}
As you can see, it has visibility: hidden; and opacity: 0; at first.
Then I have this in the modal component:
componentDidMount() {
this.emailInput.focus();
}
componentDidUpdate() {
this.emailInput.focus();
}
The componentDidMount() hook works as intended, the input gets focused, but the componentDidUpdate() hook does not work (as though it gets called, only the focus() doesn't work as intended).
I've managed to find the reason and it happens to be the CSS animation. If I remove the visibility animation it works (but the animation breaks, of course). I've also managed to make it work using a setTimeout() inside the hook as bellow:
componentDidUpdate() {
setTimeout(() => {
this.emailInput.focus();
}, 100);
}
This also works, but feels super hackish.
Is there any better way to achieve this? (I'm currently setting a timeout as a hack solution)

CSS Module composition

I've got a question about composition I'm hoping someone can help me with.
I'm using react-css-modules with Sass, and I'd like to know the best way to compose things for one of our basic bottom-level components.
Here's our component:
import React, {PropTypes} from 'react'
import cssModules from 'react-css-modules'
import styles from './style.sass'
const Button = ({children = 'Submit', ...props}) => {
const align = props.align ? `-${props.align}` : ''
const type = props.type ? `-${props.type}` : ''
const styleName = `button${type}${align}`
return (
<button onClick={props.onClick} {...props} styleName={styleName}>
{children}
</button>
)
}
Button.propTypes = {
align: PropTypes.string,
onClick: PropTypes.func.isRequired,
type: PropTypes.string,
}
export default cssModules(Button, styles)
And here's the stylesheet so far:
#import "~components/styles/variables"
.button
color: $button-default
background-color: transparent
font-family: $font-family
font-size: $default-font-size
font-weight: $font-regular
line-height: $default-button-height
margin: 0 $pad 0 0
outline: none
padding: 0 $pad*2
.left
float: left
.right
float: right
.primary
color: $background-interaction
background-color: $button-default
.button-left
composes: button, left
.button-right
composes: button, right
.button-primary
composes: button, primary
.button-primary-left
composes: button, primary, left
.button-primary-right
composes: button, primary, right
Right now, it's pretty painful. Every configurable prop we add exponentially increases the number of composed classes we have to provide. We can currently configure align and type, and since both can be null we have 6 possible combinations, so 5 composed classes to create in addition to the base .button.
If we added just one more prop, say just a boolean bold, we now have to add a whole bunch of new composed class names: .button-bold, .button-left-bold, .button-right-bold, .button-primary-bold, .button-primary-left-bold, .button-primary-right-bold.
I know with react-css-modules we can just enable the allowMultiple setting to allow us to specify multiple modules to apply to an element, but my understanding is that is against best practices. I feel like we have to be missing something here. What are we doing wrong?
I think maybe there's a mix of concerns in the example, and that's why it doesn't fit the "one class per element" rule. One of the reasons to enforce the one class per element rule is so that we can easily change the appearance of an element's state without actually touching the element. (Basically the promise of CSS itself, finally realized.) If you have multiple classes on an element, it's difficult to control the appearance without changing the element. This is especially true if you use appearance (rather than semantic) classes.
A class like "button-primary-left-bold" has some semantic meaning ("button-primary"), but it also has some layout meaning ("left") and some text appearance meaning ("bold"). A Button component probably has no business controlling its own layout. Remember, you can compose React components too! So you can have something more like:
<Left><Button type="primary">Click Me!</Button></Left>
Now the CSS module for the Button component can worry just about the types of buttons. And the Button component can be used anywhere, with any layout, not just a float-based layout.
Even better, the float could be pushed into a more semantic component as well. Perhaps something like:
<ButtonBar>
<Button>Cancel</Button>
<Button type="primary">Save</Button>
</ButtonBar>
The ButtonBar can have its own CSS module that does the layout. And later you can swap out that janky float layout for a swanky flexbox layout. :-)
Your "bold" modifier example certainly has no place inside the Button component. Better to think about why something is bold, and then make a component or semantic property for that. Otherwise, if the design calls for changing those "bold" buttons to "italic green" buttons, you have to go around changing a bunch of elements.
If you do this (exchange visual/layout classes for semantic components and break up components and CSS modules), you'll have less "exponential increases". If you do end up in a situation where you really do have a need for multiple semantic properties being combined, there is still some value in having those states be spelled out. A good example might be "primary-disabled". This is better than "primary disabled" for a couple of reasons. First, it's easy to look in the CSS module and see the explicit state listed. Second, there aren't ambiguous uses and relationships of those classes. Is "primary disabled" actually a valid use of those classes? That can only be known if somebody documents that use. The "disabled" class may override something in the "primary" class, meaning there are implicit ordering dependencies. It's easy for a well-meaning edit in the future to break things, because the relationship between those classes isn't obvious. As is often the case, choosing something implicit to save some keystrokes can lead to subtle errors. Paying that bit of keystroke tax locks things in and makes it obvious what will work and what won't.
Another small point that will save you a few keystrokes: there's really no reason for the "button-" prefix. That's exactly what CSS modules are for. Do this instead:
.normal
color: $button-default
background-color: transparent
font-family: $font-family
font-size: $default-font-size
font-weight: $font-regular
line-height: $default-button-height
margin: 0 $pad 0 0
outline: none
padding: 0 $pad*2
.primary
composes: normal
color: $background-interaction
background-color: $button-default
The filename itself essentially is the "button" prefix.
I think I'd move away from compose in this case and nest your classes. Here's my suggestion (pardon me if my jsx is a tad off):
import React, {PropTypes} from 'react'
import cssModules from 'react-css-modules'
import styles from './style.sass'
const Button = ({children = 'Submit', ...props}) => {
const align = props.align ? `${props.align}` : ''
const type = props.type ? `${props.type}` : ''
const styleName = `button ${type} ${align}`
return (
<button onClick={props.onClick} {...props} styleName={styleName}>
{children}
</button>
)
}
Button.propTypes = {
align: PropTypes.string,
onClick: PropTypes.func.isRequired,
type: PropTypes.string,
}
export default cssModules(Button, styles)
And the SASS:
#import "~components/styles/variables"
.button
color: $button-default
background-color: transparent
font-family: $font-family
font-size: $default-font-size
font-weight: $font-regular
line-height: $default-button-height
margin: 0 $pad 0 0
outline: none
padding: 0 $pad*2
&.left
float: left
&.right
float: right
&.primary
color: $background-interaction
background-color: $button-default
Fully acknowledging that "left" and "primary" could conflict with other class names in your app. So it might not be a bad idea to come up with some slightly better (more scoped) names.

How to properly translate shadow DOM CSS selectors to non-shadow-DOM selectors

I want to test Polymer applications with non-Shadow-DOM capable browsers like Firefox, PhantomJS, and maybe others using WebDriver.
WebDriver commands for Firefox and PhantomJS fail when I use something like
driver.findElement(const By.cssSelector('* /deep/ #some-div'));
Are there some rules how to best translated/approximate these selectors when the polyfills can not be applied:
/deep/
::shadow
:host()
:host-context()
:content
I would like to create a function that translates such selectors automatically to non-shadow-DOM selectors for browsers that don't support them before sending the request and for that I need to know how to translate them.
Question is a bit old, but in case you haven't figured it out yourselves yet.
/deep/ (deprecated): As you said in your answer, just removing it should work in most of the cases.
::shadow (deprecated): Can also just be removed. Replacing it with > might not work if node which you are targeting is not an immediate child of host element's shadow root.
:host() pseudo classes is used to select custom element from inside shadow-dom, in non-supported browsers it will be equal to selecting parent from child element. Since we don't have parent selectors in css and you are writing js for conversion, you can identify tagName of host element and use it instead of :host selector. Something like below:
:host {
opacity: 0.4;
transition: opacity 420ms ease-in-out;
}
:host(:hover) {
opacity: 1;
}
:host(:active) {
position: relative;
top: 3px;
left: 3px;
}
/*Convert it to*/
x-element {
opacity: 0.4;
transition: opacity 420ms ease-in-out;
}
x-element:hover {
opacity: 1;
}
x-element:active {
position: relative;
top: 3px;
left: 3px;
}
:host-context(<selector>) pseudo class matches the host element if it or any of its ancestors matches <selector>. for example:
Below rule will apply on custom element only when it's a descendant of an element with the class .different.
:host-context(.different) {
color: red;
}
<body class="different">
<x-foo></x-foo>
</body>
It won't be very easy to replace this one with anything simple. Even webcomponents polyfill doesn't attempt it. I can't think of any css only way to achieve this.
::content targets distributed child nodes of host element, i.e. all elements which are picked to display using content selectors. Replacing ::content selectors with tagName of host elements should work here. i.e.
::content > h3 {
color: green;
}
/*replace it with*/
x-element h3 {
color: green;
}
Note that I have removed child selector > also from above, because in non-supported browsers after distribution h3 won't be a direct descendant of x-element anymore. Given the way content selector is used, I'd suggest removing child selector also wherever available.
/deep/ can just be removed
::shadow can be replaced by >
don't know about the others yet

Resources