Styled Components - content passed through props (content) - reactjs

I am trying to toggle text inside a div (styled component) depending on the props value. The idea is simple: state changes, its value goes to props of the styled component, and then depending what it is, the content adjusts. What is not working is the content in before / after. Any idea why?
STYLED COMPONENT:
export const Info = styled.div`
padding: 5px;
margin-top: 15px;
&::after {
content: ${props => props.content === "intro" && "hello"};
}
`
JS
const CheckNumber = () => {
const [msg, setMsg] = useState("intro")
return (
<Info content={msg}/>
)
}
export default CheckNumber

Your current code generates style
content: hello
that is invalid CSS. You need extra quotes(one pair for JS level and nested quotes for CSS):
&::after {
content: ${props => props.content === "intro" && "'hello'"};
}
PS found that really accidentally by duplicating content like:
&::after {
content: "aaaa";
content: ${props => props.content === "intro" && "hello"};
}
because browser just did not create "::after" prseudoelement while content is invalid - so we were unable to check what styles were actually generated.

Related

How to avoid long conditional rendering with styled components?

I'm working on a project using a lot of different variables. Depending on props, they will render a certain color (for example). The problem is that with the amount of variables my code ends up really long. Is there a solution to clean up this code?
This is an example:
Variables:
const Variables = {
Colors: {
Primary50: "rgb(244, 246, 247)",
Primary100: "rgb(209, 218, 225)",
...rest of colors
},
...rest of variables
}
And the styled component:
const Component = styled.div<{ $color: number }>`
background-color: ${({ $color }) =>
$color === 50
? Variables.Colors.Primary50
: $color === 100
? Variables.Colors.Primary100
: ...rest};
`
I tried something like this, but it is not working:
const Component = styled.div<{ $number: number }>`
background-color: ${Variables.Colors.Primary[$number]};
`
Thanks for your answers!
Better use switch instead of ternary operator for your instance. The second aproach Primary[$number] is wrong, it look like Variables.Colors.Primary.100 you need to enclose Primary50 entirely in square brackets.
const Variables = {
Colors: {
Primary50: 'rgb(244, 246, 247)',
Primary100: 'rgb(209, 218, 225)',
},
};
const Component = styled.div<{ $color: number }>`
background-color: ${props => {
switch (props.$color) {
case 50:
return Variables.Colors.Primary50;
case 100:
return Variables.Colors.Primary100;
default:
return 'white';
}
}};
`;
const Component2 = styled.div<{ $number: 50 | 100 }>`
background-color: ${props => Variables.Colors[`Primary${props.$number}`]};
`;
function App() {
return (
<div className="App">
<Component $color={100}>First example</Component>
<Component2 $number={50}>Second example</Component2>
</div>
);
}
export default App;

My React component renders before the props data is available using hooks

So I'm creating a simple tile matching app.
I have a tile component that is fed props from a game board component that houses the tiles and that itself is within the app obviously that has the main logic and there state data.
I need each Tile to have its own logic to know if it should be flipped or not.
The problem I have encountered is that when I create a state data for which tiles are flipped and/or should stay flipped the tiles are running their component level logic with previous render data.
This means I match two tiles, these are added to relevant state - selectedTiles and matchedTiles which is passed to the all the tiles. The tiles check if there are two values in selectedTiles and if so to then check matchedTiles to see if that value matches a component level prop.
however the logic to check matchedTiles, which is triggered by selectedTiles changing, runs before the matchedTiles prop reaches the component.
I understand that this is because of the snapshot of the state but I'm unsure how to actually solve the problem.
apologies for any misunderstanding or explaining as this is my first fully self written react app.
I have linked the repository on git below.
https://github.com/Samuel-Programmer/React-Tiles
I pulled you repo and changed the code a little bit.
In my opinion, it is better to hold 2 separate variables with useState, one for the opened tile and one for the 2nd 'guess'.
I also added a setTimeout to hide both open tiles after 1 second, in case they do not match.
I added an 'awaitingFlip' variable to prevent the user from flipping more tiles while the 2 last tiles have not been flipped back on a wrong guess.
Passing all the information in each tile is bad coding. The app gets slower, since react has to calculate more stuff. I added a prop 'isFlipped' to each tile, which is true in case two tiles have been matched or a tile has been checked as a first or second guess.
I also added a 'tileId' property to each tile, because I do not like working with indexes as ids.
I changed the unsplash search from 'south africa' to 'patagonia', 'cause I was getting a duplicate image which was frustrating. I advise you to hide your unsplash api key from the repo and generate a new one, as people will be able to use it.
You should be able to do the rest of the work (like 'play again', 'reshuffle', etc...).
I hope I helped. If you need any more help, leave a comment.
Here's the code (the files I do not include are the same):
App.js:
import React, { useState, useEffect } from "react";
import styled from "styled-components";
import unsplash from "../api/unsplash";
import Header from "./Header";
import GameBoard from "./GameBoard";
function App() {
const [searchTerm, setSearchTerm] = useState("patagonia");
const [images, setImages] = useState([]);
const [randomisedImages, setRandomisedImages] = useState([]);
useEffect(() => {
randomiseImages(images);
}, [images]);
useEffect(() => {
getImages();
}, [searchTerm]);
const generateTileId = () => {
return 'tile_id_' + Math.random().toString().substr(2, 8);
}
const getImages = async () => {
const response = await unsplash.get("/search/photos", {
params: { query: searchTerm },
});
setImages(response.data.results);
};
const randomiseImages = (images) => {
let randomizedImages = [...images, ...images];
var m = images.length,
t,
i;
while (m) {
i = Math.floor(Math.random() * m--);
t = randomizedImages[m];
randomizedImages[m] = randomizedImages[i];
randomizedImages[i] = t;
}
let finalArray = [];
for (let x of randomizedImages) {
finalArray.push({
...x,
tileId: generateTileId()
})
}
setRandomisedImages([...finalArray]);
};
return (
<div>
<Container>
<Header />
<Main>
<GameBoard images={randomisedImages} />
</Main>
</Container>
</div>
);
}
export default App;
const Container = styled.div`
width: 100%;
height: 100vh;
display: grid;
grid-template-rows: 7rem;
`;
const Main = styled.div`
display: grid;
grid-template-columns: auto;
`;
GameBoard.js:
import React, { useState, useEffect } from "react";
import styled from "styled-components";
import Tile from "./Tile";
function GameBoard({ images }) {
const [selectedTile, setSelectedTile] = useState(null);
const [secondTile, setSecondTile] = useState(null);
const [matchedTiles, setMatchedTiles] = useState([]);
const [awaitingFlip, setAwaitingFlip] = useState(false);
useEffect(() => {
if (matchedTiles.length > 0 && matchedTiles.length === images.length / 2) {
alert('YOU WON!!! :)');
}
}, [matchedTiles])
const onTileClick = (tileId, id) => {
if (!matchedTiles.includes(id) && tileId !== selectedTile && !awaitingFlip) {
let subjectId = images.find(x => x.tileId === selectedTile)?.id;
if (!selectedTile) {
setSelectedTile(tileId);
} else {
if (id === subjectId) {
setMatchedTiles([...matchedTiles, id]);
setSelectedTile(null);
} else {
setSecondTile(tileId);
setAwaitingFlip(true);
setTimeout(() => {
setSecondTile(null);
setSelectedTile(null);
setAwaitingFlip(false);
}, 1000);
}
}
}
}
return (
<TileContainer>
{images.length > 1 &&
images.map(({ description, urls, id, tileId }, index) => (
<Tile
key={index}
id={id}
tileId={tileId}
alt={description}
src={urls.regular}
onTileClick={onTileClick}
isFlipped={selectedTile === tileId || secondTile === tileId || matchedTiles.includes(id)}
/>
))}
</TileContainer>
);
}
export default GameBoard;
const TileContainer = styled.div`
background: black;
padding: 7rem;
display: flex;
justify-content: space-evenly;
align-items: center;
flex-wrap: wrap;
`;
Tile.js:
import React, { useState, useEffect, useLayoutEffect } from "react";
import tile from "./tile.jpeg";
import styled from "styled-components";
function Tile({ id, alt, src, onTileClick, tileId, isFlipped }) {
return (
<Imgcontainer onClick={() => onTileClick(tileId, id)}>
<img alt={alt} src={isFlipped ? src : tile} id={id} />
</Imgcontainer>
);
}
export default Tile;
const Imgcontainer = styled.div`
height: 10rem;
width: 10rem;
margin: 1rem;
border: 2px solid white;
border-radius: 10%;
cursor: pointer;
img {
width: 100%;
height: 100%;
border-radius: 10%;
}
img:hover {
transform: scale(1.1);
box-shadow: 0 8px 15px #dcfffe;
border: 2px solid white;
border-radius: 10%;
}
`;

Which is a better to write a react component with styled-components?

Given some buttons to be shared across a website which one of these two methods is best to use:
const ButtonA = styled.button`
color: 'red'
`
const ButtonB = styled.button`
color: 'blue'
`
to be used like this
const Home = () => {
return <ButtonA>Button A</ButtonA><ButtonB>Button B</ButtonB>;
}
or
const Button = styled.button`
${({variant }) => variant === 'A' && `
color: red;
`
${({variant }) => variant === 'B' && `
color: blue;
`
`
to be used like this:
const Home = () => {
return <Button variant="A">Button A</Button><Button variant="B">Button B</Button>;
}
Please give reasons, advantages and disadvantages for using one method over the other.
As always depends on the case.
It's usually better to pass variant prop as in your second example.
The problem with the first solution is that you have to repeat the same code. Better to do it like this:
const Button = styled.button`
// basic styles here
`
const ButtonWithDifferentStyles = styled(Button)`
// additional styles here
`

How to pass prop in styledcomponent to stylerule?

I am using styledcomponents in my reactjs app. In one of the components I have a mediarule:
#media (max-width: 1450px)
{
padding-right: ${props => (props.meta.count > 0 ? '6px' : '0')};
}
This meta.count prop does exist but when I run the app I get:
TypeError: Cannot read property 'count' of undefined
The render starts like this:
const { lastUpdated, meta } = this.props;
The meta prop gets loaded through a redux selector:
const mapStateToProps = state => ({
meta: getAlertsMeta(state),
});
How can I avoid this / fix this?

Warning: Received `false` for a non-boolean attribute. How do I pass a boolean for a custom boolean attribute?

Warning: Received `false` for a non-boolean attribute `comingsoon`.
If you want to write it to the DOM, pass a string instead:
comingsoon="false" or comingsoon={value.toString()}.
How do I pass a boolean in a custom attribute for React?
I'm using styled-components and passing the attribute through the component. Here is a picture of how I'm passing the attr.
passing boolean custom attr as "comingsoon"
styled-components css props
Try this instead:
comingsoon={value ? 1 : 0}
As of 5.1 you can now use transient props ($) which prevents the props being passed to the DOM element.
const Comp = styled.div`
color: ${props =>
props.$draggable || 'black'};
`;
render(
<Comp $draggable="red" draggable="true">
Drag me!
</Comp>
);
You have to add $ prefix to attribute:
$comingsoon={value}
Styled Components had added transient props in 5.1 version:
https://styled-components.com/docs/api#transient-props
In my case, it was because I was accidentally passing {...#props} down into a div.
Usually passing attribute={false} is fine, but not to native elements.
Similar to Frank Lins answer above but I had to use undefined instead of 0 to get rid of the warning:
comingsoon={value ? 1 : undefined}
Just make it a number instead, this is the workaround from https://github.com/styled-components/styled-components/issues/1198:
This error with styled-components appears to be due to styled() attempting to apply a boolean to an element in the DOM, but DOM elements only accept strings as attributes.
This is well documented in the styled-components repository here: https://github.com/styled-components/styled-components/issues/1198
There are two solutions:
Lift the styled component w/ the passed attribute up, so that the attribute is not applied to the element directly. Or,
Filter the passed attribute out of the props when calling styled components.
Both of these options are demonstrated in the code below.
CodeSandbox: https://codesandbox.io/s/cool-thunder-9w132?file=/src/App.tsx
import React, { useState } from "react";
import styled from 'styled-components';
// demonstration of two different solutions for solving the styled-components error:
// `Warning: Received `false` for a non-boolean attribute`
// Solution 1: Lift the attribute up into a wrapper.
// Solution 2: Filter-out the `toggle` attribute passed to styled-component.
interface BtnProps {
toggle: boolean;
}
const Container = styled.div`
width: 100%;
height: 500px;
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: center;
`;
const StyledBtnOne = styled.div<BtnProps>`
& button {
background-color: ${({toggle}) => toggle ? ' #2ecc71' : '' };
};
`;
const StyledBtnTwo = styled(({primary, ...props}) =>
<button {...(({toggle, ...propz}) => propz)(props)} />)<BtnProps>`
background-color: ${({toggle}) => toggle ? ' #3498db' : '' };
`;
const App = () => {
const [ btnOne, setBtnOne ] = useState(false);
const [ btnTwo, setBtnTwo ] = useState(false);
const triggerOne = () => setBtnOne(!btnOne);
const triggerTwo = () => setBtnTwo(!btnTwo);
return (
<Container>
<StyledBtnOne toggle={btnOne}>
<button
onClick={triggerOne}>
Solution 1
</button>
</StyledBtnOne>
<StyledBtnTwo
toggle={btnTwo}
onClick={triggerTwo}>
Solution 2
</StyledBtnTwo>
</Container>
);
}
export default App;
This warning can be caused also if the property of styled component has the name existing in HTML. For example I had such issue when named property wrap. After renaming warning disappeared.
Add "+" before your booleans values.
comingsoon = {+creator.comingSoon}
example below from the Link to answer
import styled from "styled-components";
import { Link } from "react-router";
const StyledLink = styled(Link)`
color: ${({ inverted }) => (inverted ? "white" : "chartreuse")};
`;
function Navbar() {
return (
<nav>
{# Bad #}
<StyledLink inverted={true}>Home</StyledLink>
{# Good #}
<StyledLink inverted={+true}>About</StyledLink>
</nav>
);
}
Solved this by enclosing with brackets {} the boolean variable I was passing through props.
const childrenWithProps = React.Children.map(props.children, child => {
return React.cloneElement(child, { showcard: { showCard } }
)});
I got this issue and also shows React Hydration Error in my Next.js application. In my case it seems Styled component got a custom component and it can't process 'boolean'.
Here is my workaround:
before:
styled(Text)<{ center?: boolean}>
// Text is my custom component
after:
styled.div<{ center?: boolean}>

Resources