Dynamically change styled component based on state AND index - javascript

So I have a list that was returned from an API request (not important)
lets call it list = [1,2,3,4,5,6,7];
Now, inside render(), I have something like the following
render(){
<Wrapper>
{list.map((i) => {
return (<Button id = {i} onClick = {this.customFunc.bind(this, i)} />)
}
</Wrapper>
}
Now, I have another list, lets call it list_check = [false...] (for all 7 elements listed above)
Assume that customFunc changes the respective button id in list_check from false to true.
e.g. if I clicked button 1 (id = 1) then list_check becomes [false, true, false...]
** Now my problem is: I have 2 styled components, Button and Button_Selected,
how can i dynamically change between the 2 styled components so that if that unique button is clicked (change list_check[index] = true), the element becomes Button_Selected instead of Button (The entire list is initalized as Button since all elements are false)
Just to make it clear:
Both arrays are located in this.state and by 2 styled components I mean there exists
const Button = styled.div`
//styling here
`;
and
const Button_Selected = Button.extend`
//Small styling change to differentiate between selected and not selected
`;
edit: all answers are great! too bad I can only select one :(

You could just save the component to another variable.
this.state.list_check.map((item, i) => {
const NecessaryButton = item ? SelectedButton : Button;
return <NecessaryButton onClick={() => this.makeSelected(i)}>Normal Button</NecessaryButton>
})
You can see a live example here.

Although you can return 2 buttons based on conditional rendering .You can also pass props to your styled Button so that based on props you can change your styles.
render(){
<Wrapper>
{list.map((i) => {
return (<Button id = {i} isSelected={this.state.list_check[i]} onClick = {this.customFunc.bind(this, i)} />)
}
</Wrapper>
}
And in your styled Button:
const Button = styled.div`
styleName: ${props => props.isSelected ? 'styling_if_Selected' : 'styling_if_not_selected'};
`;

The easiest approach would be to have a single Button component and handle in the state if it was selected. Depending on this state you could switch classes.
Example:
class Button extends React.Component {
state = {
isSelected: false
};
handleClick() {
//Here we will set the state change and call the onClick function passed by props
this.setState({isSelected: !this.state.isSelected}, () => { this.props.onClick(); });
}
render() {
return (
<button
class={this.state.isButtonSelected ? "classBtnSelected" : "classBtnDefault"}
onClick={this.handleClick}
/>
);
}
}
Still, if you want to switch components you can use state to control if it was selected and do a conditional rendering. Example:
render(){
<Wrapper>
{list.map((i) => {
return (this.state.isSelected
? <Button id={i} onClick = {this.customFunc.bind(this, i)} />
: <Button_Selected id={i} onClick = {this.customFunc.bind(this, i)} />)
}
</Wrapper>
}

Related

Switching classes between items from the list in React

im doing this simple ecommerce site, and on the product page you have different attributes you can choose, like sizes, colors - represented by clickable divs, with data fetched from GraphQl and then generated to the DOM through map function.
return (
<div className="attribute-container">
<div className="attribute">{attribute.id.toUpperCase()}:</div>
<div className="attribute-buttons">
{attribute.items.map((item) => {
if (type === "Color") {
return (
<AttributeButton
key={item.id}
className="color-button"
style={{ backgroundColor: item.value }}
onClick={() => addAtribute({type: type, value: item.value })}/>
);
}
return (
<AttributeButton
key={item.id}
className="size-button"
size={item.value}
onClick={() => addAtribute({ type: type, value: item.value })}
/>
);
})}
</div>
</div>
);
Im importing external component, then I check if an attribute's type is Color (type color has different styling), then render depending on that type.
What I want to implement is that when i click on one attribute button, its style changes, BUT of course when i click on another one, choose different size or color for the item i want to buy, new button's style changes AND previously selected button goes back to it's default style. Doing the first step where buttons style changes onClick is simple, but i cant wrap my head around switching between them when choosing different attributes, so only one button at the time can appear clicked.
Here is code for AttributeButton:
class Button extends PureComponent {
constructor(props){
super(props);
this.state = {
selected: false,
}
}
render() {
return (
<div
className={ !this.state.selected ? this.props.className : "size-selected "+this.props.className}
style={this.props.style}
onClick={() => {this.props.onClick(); this.setState({selected: !this.state.selected}) }}
>
{this.props.size}
</div>
);
}
}
export default Button;
PS - i have to use class components for this one, it was not my choice.
You need to have the state selected outside of your <Button> component and use it as a prop instead. Something like:
handleSelect = (button) => {
const isSelected = this.state.selected === button;
this.setState({ selected: isSelected ? null : button });
};
render() {
return (
<>
<Button
isSelected={this.state.selected === "ColorButton"}
onClick={() => this.handleSelect("ColorButton")}
/>
<Button
isSelected={this.state.selected === "SizeButton"}
onClick={() => this.handleSelect("SizeButton")}
/>
</>
);
}

Setting Selected State to Mapped Components

I have a mapped component which iterates through API data. It passes props to each one and therefore each card looks different. See example below.
https://gyazo.com/39b8bdc4842e5b45a8ccc3f7ef3490b0
With the following, I would like to achieve two goals:
When the component is selected, it uses state to STAY SELECTED, and changes the colour as such to lets say blue for that selected component.
I hope this makes sense. How do I index a list as such and ensure the colour and state remains active based on this selection?
See below.
The level above, I map the following cards using these props.
{
jobs.length > 0 &&
jobs.map(
(job) =>
<JobCard key={job.id} job={job}
/>)
}
I am then using the following code for my components:
const JobCard = ({ job }) => {
const responseAdjusted = job.category.label
const responseArray = responseAdjusted.split(" ")[0]
return (
<CardContainer>
<CardPrimary>
<CardHeader>
<CardHeaderTopRow>
<Typography variant = "cardheader1">
{job.title}
</Typography>
<HeartDiv>
<IconButton color={open ? "error" : "buttoncol"} sx={{ boxShadow: 3}} fontSize ="2px" size="small" fontSize="inherit">
<FavoriteIcon fontSize="inherit"
onClick={()=> setOpen(prevOpen => !prevOpen)}/>
</IconButton>
</HeartDiv>
</CardHeaderTopRow>
<Typography variant = "subtitle4" color="text.secondary">
{job.company.display_name}
</Typography>
</CardHeader>
<CardSecondary>
</CardSecondary>
</CardPrimary>
</CardContainer>
)
}
You can attach a handler on the <CardPrimary> component by passing a function to the onClick event. That way whenever you click anywhere on the card div, the function will be triggered.
const [isSelected, setIsSelected] = useState(false);
<CardPrimary onClick={() => setIsSelected(true)} className={isSelected ? "css-class-to-highlight-div" : undefined>
....
</CardPrimary>
If I'm understanding what you're asking for, which I believe is to have your component be highlighted when it is clicked, then you need to modify the 'CardContainer' component to render with an 'onClick' parameter.
Example:
function CardContainer(props) {
const cssClass = 'highlighted';
const my_id = props.id || 'need_an_id';
var clearExistingHighlight = () => [...document.getElementByClassName(cssClass)].forEach((elem)=>elem.classList.remove(cssClass));
var isHighlighted = () => document.getElementById(my_id).classList.contains(cssClass);
var setHighlighted = (e) => {
clearExistingHighlight();
e.target.classList.add(cssClass);
}
return (
<div id={my_id} onClick={setHighlighted}>Cheeseburger fry</div>
)
}
If you don't want the highlight to disappear, you can get rid of the clearExistingHighlight function. Or if you want it to toggle, I recommend a modification of #sid's answer:
const {useState} = React;
function CardContainer(props) {
const [isSelected, setIsSelected] = useState(false);
<div onClick={() => setIsSelected(!isSelected)} className={isSelected ? "highlighted" : undefined>
}
style.css:
.highlighted {
background-color: 'orange';
}
You can do all of this without any react hook and rely instead on CSS classes. You can use the 'isHighlighted' method to determine if a given component is highlighted or not.

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])

Select Next/Previous list item in React without re-rendering

I'm learning React. Say that I have a ListContainer which renders several ListItems. I want to keep track of the currently selected ListItem, render it in another color, and be able to navigate up and down.
One way would be to store selectedItem as state in ListContainer, and send it down as a prop to ListItem. But if I do it this way, then every time I change selectedItem I will rerender all ListItems because they are dependent on selectedItem. (I should only have to re-render two ListItems, the one that gets deselected, and the one that gets selected).
Is there a way to implement next and previous function without re-rendering all items?
Note: I know that React doesn't re-render unnecessarily in the DOM, but I'm trying to optimize operations on virtual DOM also.
Edit: Here is my example in code. It renders a list, and when the user click one item it gets selected. We also see that "ListItem update" gets printed 100 times, each time we change selection, which happens regardless of PureComponent or React.memo.
let mylist = []
for (let i = 0; i < 100; i++) {
mylist.push({ text: "node:" + i, id: i })
}
window.mylist = mylist
const ListItem = React.memo (class extends Component {
componentDidUpdate() {
console.log('ListItem update')
}
render() {
let backgroundColor = this.props.item.id === this.props.selectedItem ? 'lightgreen' : 'white'
return (
<li
style={{ backgroundColor }}
onMouseDown={() => this.props.setSelected(this.props.item.id)}
>
{this.props.item.text}
</li>
)
}
})
class ListContainer extends Component {
constructor(props) {
super(props)
this.state = {
selectedItem: 10
}
this.setSelected = this.setSelected.bind(this)
}
setSelected(id) {
this.setState({ selectedItem: id })
this.forceUpdate()
}
render() {
return (
<ul>
{this.props.list.map(item =>
<ListItem
item={item}
key={item.id}
selectedItem={this.state.selectedItem}
setSelected={this.setSelected}
/>)}
</ul>
)
}
}
function App() {
return (
<ListContainer list={mylist} />
);
}
The state you suggesting is the right way to implement it...
The other problem with unnecessary renders of the list item can easily be soved by wrapping the export statement like this:
export default React.memo(ListItem)
This way the only elements that has changed their props will rerender.. be aware that overuse of This can cause memory leaks when using it unnecessarily...
UPDATE
according to your example in addition to the React.memo you can update the way you transfer props to avoid senfing the selected item in each item...
istead of:
let backgroundColor = this.props.item.id === this.props.selectedItem ? 'lightgreen' : 'white'
...
<ListItem
item={item}
key={item.id}
selectedItem={this.state.selectedItem}
setSelected={this.setSelected}
/>)}
do :
let backgroundColor = this.props.selectedItem ? 'lightgreen' : 'white'
...
<ListItem
item={item}
key={item.id}
selectedItem={item.id === this.state.selectedItem}
setSelected={this.setSelected}
/>)}
this way the react memo will prevent rerenders when it is possible...

Disable buttons from parent

I am creating a Quiz app for the sake of learning React Native.
I want that when a user presses an answer, all buttons should be disabled. I have no idea how to do this, I have tried all different approaches, like changing props of the buttons from the parent, setting state from the parent etc. I just can't figure it out. I can make the clicked button disabled, but that doesn't help since the other buttons are still clickable.
Parent
class Container extends Component {
state = { currentQuestion: questions[0] }
buttons = new Array();
componentWillMount() {
this.makeButtons();
}
makeButtons() {
for (let i = 0; i < 4; i++) {
const isCorrect = (i === 0); //the first answer is correct, this is how I keep track
const btn = (
<Button
key={i}
title={this.state.currentQuestion[i]}
isCorrect={isCorrect}
/>
);
this.buttons.push(btn);
}
shuffle(this.buttons);
}
render() {
return (
<View style={containerStyle}>
<Text style={textStyle}>
{this.state.currentQuestion.title}
</Text>
{this.buttons}
</View>
);
}
}
Button
class Button extends Component {
state = { color: "rgb(0,208,196)" };
handleEvent() {
const newColor = (this.props.isCorrect) ? "green" : "red";
this.setState({ color: newColor });
this.props.onPress();
}
renderButton() {
return (
<TouchableOpacity
style={buttonStyle}
onPress={this.handleEvent.bind(this)}
disabled={this.props.disabled}
>
<Text style={textStyle}>
{this.props.title}
</Text>
</TouchableOpacity>
);
}
render() {
return this.renderButton();
}
}
You are creating your button components within an instance variable once when the parent component loads, but never re-rendering them. This is an anti-pattern of React. Ideally, your components should all be rendered within render(), and their props should be computed from state, so you only need to worry about updating the state correctly and all your components render properly.
In this case, you should construct the data for your buttons at component load, save your button data within state, and render your buttons within render(). Add a "disabled" state to your Button component, and when a user presses one of the buttons, use a callback to set "disabled" state in the parent component, and all your buttons will re-render to be properly disabled.

Categories

Resources