The SelectedColumn value doesn't come in the CustomHeader component. However, setSelectedColumn works! Why🧐 ?
Also, I'm passing CustomHeader to constant components that use useMemo. Without useMemo CustomHeader doesn't work.
const [selectedColumn, setSelectedColumn] = useState(null);
console.log("selected Column Outside:", selectedColumn); // It works!
const CustomHeader = (props) => {
const colId = props.column.colId;
console.log("selected Column In CustomHeader:", selectedColumn); // Doesn't work
return (
<div>
<div style={{float: "left", margin: "0 0 0 3px"}} onClick={() => setSelectedColumn(props.column.colId)}>{props.displayName}</div>
{ selectedColumn === colId ? <FontAwesomeIcon icon={faPlus} /> : null}
</div>
)
}
const components = useMemo(() => {
return {
agColumnHeader: CustomHeader
}
}, []);
UPDATE: If I use the useState hook inside the CustomHeader component, it adds a "+" sign to each column and does not remove from the previous one. Here is a picture:
After reading your comment, your issue is clearly about where you want to place your useState.
First of all, you should always place useState inside a component. But in your case, apparently what you're trying to achieve is that when you select a column, the other columns get deselected.
Therefore, you need to pass both selectedColumn and setSelectedColumn as props to your component, and create the useState on the parent component.
Assuming all your CustomHeader components share the same parent component, in which my example I'll call CustomHeadersParent, you should do something like this:
// added mock headers to have a working example
const headers = [
{
displayName: "Game Name",
column: {
colId: 1,
},
},
{
displayName: "School",
column: {
colId: 2,
},
},
];
const CustomHeadersParent = (props) => {
const [selectedColumn, setSelectedColumn] = useState(null);
return headers.map((h) => (
<CustomHeader
column={h.column}
displayName={h.displayName}
setSelectedColumn={setSelectedColumn}
selectedColumn={selectedColumn}
/>
));
};
const CustomHeader = (props) => {
const colId = props.column.colId;
return (
<div>
<div
style={{ float: "left", margin: "0 0 0 3px" }}
onClick={() => props.setSelectedColumn(props.column.colId)}
>
{props.displayName}
</div>
{props.selectedColumn === colId ? <FontAwesomeIcon icon={faPlus} /> : null}
</div>
);
};
const components = useMemo(() => {
return {
agColumnHeader: CustomHeader,
};
}, []);
You should use hooks inside your component
const CustomHeader = (props) => {
const colId = props.column.colId;
const [selectedColumn, setSelectedColumn] = useState(null);
console.log("selected Column In CustomHeader:", selectedColumn); // Should work
return (
<div>
<div style={{float: "left", margin: "0 0 0 3px"}} onClick={() => setSelectedColumn(props.column.colId)}>{props.displayName}</div>
{ selectedColumn === colId ? <FontAwesomeIcon icon={faPlus} /> : null}
</div>
)
}
Related
I'm trying to test that a click handler in my child component that is changing the parent component's state and then displaying conditional jsx in my parent component, but I can't figure out the best way to do so and I'm also having trouble debugging. My other tests that test the parent component and child component separately are working (as in I'm able to find dom elements that I expect to be present), but when I try to test the clicking of a button in the child component by rendering the parent component, my test fails.
Expected behavior:
User clicks the div with className 'open-comparison-btn'
Child component calls the props.setModalShowing function with 'true'
Parent component modalShowing state is updated to true
Parent component re-renders and displays the conditional jsx className 'comparison-modal'
The functionality is working in the localhost browser, but not in my test, and I can't even find the child component's html at all in my test.
Parent component:
import React, { useState, useEffect } from 'react';
import ProductCard from './ProductCard.jsx';
const RelatedProducts = (props) => {
const [position, setPosition] = useState(0);
const componentName = 'RelatedProducts';
const [modalShowing, setModalShowing] = useState(false);
const [currentProduct, setCurrentProduct] = useState({});
const [comparisonProduct, setComparisonProduct] = useState({});
useEffect(() => {
setCurrentProduct(props.currentProduct);
}, [props.currentProduct]);
const getFeatures = () => {
return [...currentProduct.features, ...comparisonProduct.features]
.filter((v, i, a)=>a.findIndex(v2=>(v.feature === v2.feature && v.value === v2.value)) === i);
};
return (
<>
<div className='related-products-container' role="listbox" aria-label="related products" style={{marginLeft: `-${position}px`}}>
{props.relatedProducts ?
props.relatedProducts.map((product) => {
return <ProductCard
modalShowing={modalShowing}
setModalShowing={setModalShowing}
setComparisonProduct={setComparisonProduct}
key={product.id}
product={product}
generateStars={props.generateStars}
isFetching={props.isFetching}
setIsFetching={props.setIsFetching}
parentComponent={componentName}
yourOutfit={props.yourOutfit}
addToOutfit={props.addToOutfit}
/>;
})
: null
}
</div>
<div className='fade-top'>
{ position > 0 ?
<div className="arrow-container-left" role="button" aria-label="scroll left" onClick={() => { setPosition(position - 250); }}>
<div className="arrow-left"></div>
</div>
: null
}
{ props && props.relatedProducts && position <= (props.relatedProducts.length - 4) * 250 ?
<div className="arrow-container-right" role="button" aria-label="scroll right" onClick={() => { setPosition(position + 250); }}>
<div className="arrow-right"></div>
</div>
: null
}
</div>
{modalShowing ?
<div className='comparison-modal' role='dialog' aria-label='comparison window'>
<div className='modal-top'>COMPARING</div>
<div className='modal-product-names'>
<div className='product-1'>{currentProduct.name}</div>
<div className='product-2'>{comparisonProduct.name}</div>
</div>
<table className='modal-table'>
<tbody>
{getFeatures().map((feature, index) => {
return (
<tr key={`${feature}-${index}`}>
<td className='left-check'>{currentProduct.features.filter(item => item.feature === feature.feature && item.value === feature.value).length > 0 ? '✓' : null}</td>
<td>{feature.value} {feature.feature}</td>
<td className='right-check'>{comparisonProduct.features.filter(item => item.feature === feature.feature && item.value === feature.value).length > 0 ? '✓' : null}</td>
</tr>
);
})}
</tbody>
</table>
<div className="close-btn" onClick={() => { setModalShowing(false); }}></div>
</div>
: null}
</>
);
};
export default RelatedProducts;
Child component:
import React, { useState, useEffect } from 'react';
import ratingsAPI from '../../API/Ratings.js';
import { useNavigate } from 'react-router-dom';
const ProductCard = (props) => {
const navigate = useNavigate();
const [averageRating, setAverageRating] = useState();
const stars = props.generateStars(averageRating, 'related');
useEffect(() => {
ratingsAPI.getReviewMetadata(props.product.id)
.then((metadata) => {
setAverageRating(getAverageRating(metadata.ratings));
props.setIsFetching(false);
});
}, []);
const routeChange = () => {
const path = `/${props.product.id.toString()}`;
navigate(path);
};
const displayComparison = (e) => {
props.setComparisonProduct(props.product);
props.setModalShowing(true);
e.stopPropagation();
};
const getAverageRating = (ratings) => {
var sum = 0;
var count = 0;
Object.keys(ratings).forEach(function(rating) {
sum += rating * parseInt(ratings[rating]);
count += parseInt(ratings[rating]);
});
return sum / count;
};
return (
!props.isFetching ?
<>
<div className='product-card-container' onClick={() => routeChange(props.product.id)}>
<img className='product-card-image' src={props.product.styles.results[0].photos[0].thumbnail_url}>
</img>
{props.parentComponent === 'RelatedProducts'
?
<svg className="open-comparison-btn" role='button' aria-label='open comparison' width="20px" height="20px" viewBox="0 0 32 32" onClick={(e) => { displayComparison(e); }}>
<path fill="White" stroke="black" strokeWidth="2px" d="M20.388,10.918L32,12.118l-8.735,7.749L25.914,31.4l-9.893-6.088L6.127,31.4l2.695-11.533L0,12.118
l11.547-1.2L16.026,0.6L20.388,10.918z"/>
</svg>
:
<div className="close-btn" onClick={() => { props.removeFromOutfit(props.product); }}></div>
}
<div className='product-card-description'>
<div className='product-card-category'>{props.product.category}</div>
<div className='product-card-name'>{props.product.name}</div>
<div className='product-card-price'>${props.product.default_price}</div>
<div className='product-card-stars'>{ stars }</div>
</div>
</div>
</>
: null
);
};
export default ProductCard;
Test:
it('tests that clicking the open-comparison-btn opens the modal window', async () => {
render(<RelatedProducts
addToOutfit={() => { return; }}
yourOutfit={() => { return; }}
relatedProducts={relatedProducts}
generateStars={ generateStars }
isFetching={() => { return false; }}
setIsFetching={() => { return; }}
/>, {wrapper: Router});
fireEvent(
screen.getByRole('button', {name: 'open comparison'}),
new MouseEvent('click', {
bubbles: true,
cancelable: true,
}),
);
const modal = screen.getByRole('dialog', {name: 'comparison window'});
expect(modal).toBeInTheDocument();
});
Any advice would be appreciated.
The answer ended up being simply using await before render...
I think it might be because my child component was doing an API call, but I'm not sure why I did not need to use await when rendering and testing the child component separately.
I'm wiriting a kibana plugin and I have some problems with a flyout component. That's my starting code:
export const InputPipelineDebugger = ({queryParams, setQueryParams, setConnectionType, setMessage}) => {
const onChangeTest = (e) => {
setMessage(e.target.value);
}
const onTabConnectionTypeClicked = (tab) => {
setConnectionType(tab.id);
}
var tabsConnection = [
{
id: 'http',
name: 'HTTP',
content: <HttpInput onChangeTest = {onChangeTest} queryParams = {queryParams} setQueryParams={setQueryParams} />
},
{
id: 'syslog',
name: 'SYSLOG',
content: <SyslogInput onChangeTest = {onChangeTest} />
},
{
id: 'beats',
name: 'BEAT',
content: <BeatsInput onChangeTest = {onChangeTest} />
}
];
return (
<EuiFlexItem>
<h3>Input</h3>
<EuiTabbedContent
tabs={tabsConnection}
initialSelectedTab={tabsConnection[0]}
autoFocus="selected"
onTabClick={tab => {
onTabConnectionTypeClicked(tab);
}} />
</EuiFlexItem>
);
}
And what I want is to dynamically build the tabs array according to the response from a rest call. So I was trying to use the useEffect method and for that I change the tabsConnection with a state (and a default value, that works WITHOUT the useEffect method) but is not working at all. Console saids to me that the 'content' value from the tabs array is undefined, like if it's not recognizing the imports.
How can I achieve my goal? Thanks for the support
export const InputPipelineDebugger = ({queryParams, setQueryParams, setConnectionType, setMessage}) => {
//initialized with a default value
const [tabs, setTabs] = useState([{
id: 'syslog',
name: 'SYSLOG',
content: <SyslogInput onChangeTest = {onChangeTest} />
}]);
const onChangeTest = (e) => {
setMessage(e.target.value);
}
const onTabConnectionTypeClicked = (tab) => {
setConnectionType(tab.id);
}
useEffect(()=>{
//rest call here;
//some logics
var x = [{
id: 'beats',
name: 'BEATS',
content: <BeatsInput onChangeTest = {onChangeTest} />
}];
setTabs(x);
}, []);
return (
<EuiFlexItem>
<h3>Input</h3>
<EuiTabbedContent
tabs={tabs}
initialSelectedTab={tabs[0]}
autoFocus="selected"
onTabClick={tab => {
onTabConnectionTypeClicked(tab);
}} />
</EuiFlexItem>
);
}
Errors from the console:
Uncaught TypeError: Cannot read property 'content' of undefined
at EuiTabbedContent.render
EDIT 1
Here the code of BeatsInput and SyslogInput:
import {
EuiText,
EuiTextArea,
EuiSpacer,
EuiFlexItem,
} from '#elastic/eui';
import React, { Fragment, useState } from 'react';
export const SyslogInput = ({onChangeTest}) => {
return (
<EuiFlexItem>
<EuiFlexItem >
<EuiSpacer />
<EuiText >
<EuiTextArea fullWidth={true}
style={{ height: "450px" }}
onChange={e => onChangeTest(e)}
placeholder="Scrivi l'input"
/>
</EuiText>
</EuiFlexItem>
</EuiFlexItem>
)
}
import {
EuiText,
EuiTextArea,
EuiSpacer,
EuiFlexItem,
} from '#elastic/eui';
import React, { Fragment, useState } from 'react';
export const BeatsInput = ({onChangeTest}) => {
return (
<EuiFlexItem>
<EuiFlexItem >
<EuiSpacer />
<EuiText >
<EuiTextArea fullWidth={true}
style={{ height: "450px" }}
onChange={e => onChangeTest(e)}
placeholder="Scrivi l'input"
/>
</EuiText>
</EuiFlexItem>
</EuiFlexItem>
)
}
Change initialSelectedTab to selectedTab [or just add it in addition to]
https://elastic.github.io/eui/#/navigation/tabs
You can also use the selectedTab and
onTabClick props to take complete control over tab selection. This can
be useful if you want to change tabs based on user interaction with
another part of the UI.
Or work around:
give tabs an empty default value
const [tabs, setTabs] = useState();
render the component conditionally around tabs
{tabs && (
<EuiTabbedContent
tabs={tabs}
initialSelectedTab={tabs[0]}
autoFocus="selected"
onTabClick={tab => {
onTabConnectionTypeClicked(tab);
}}
/>
)}
Still learning some ropes of React.
I have the following code where I render a list of buttons:
import React, { useState, useEffect } from 'react';
const MyComponent = (props) => {
const [buttonList, setButtonList] = useState([]);
useEffect(() => { getButtonList()}, [])
const getButtonList = () => {
let data = [
{id: 1, name: 'One', selected: false },
{id: 2, name: 'Two', selected: false },
{id: 3, name: 'Three', selected: false }
]
setButtonList(data)
}
const ButtonItem = ({ item }) => {
const btnClick = (event) => {
const id = event.target.value
buttonList.forEach((el) => {
el.isSelected = (el.id == id) ? true : false
})
setButtonList(buttonList)
console.log('buttonList', buttonList)
}
return (
<button type="button"
className={ "btn mx-2 " + (item.isSelected ? 'btn-primary' : 'btn-outline-primary') }
onClick={btnClick} value={item.id}>
{item.name + ' ' + item.isSelected}
</button>
)
}
return (
<div className="container-fluid">
<div className="card mb-3 rounded-lg">
<div className="card-body">
{
buttonList.map(item => (
<ButtonItem key={item.id} item={item} />
))
}
</div>
</div>
</div>
)
}
export default MyComponent;
So the button renders:
[ One false ] [ Two false ] [ Three false ]
And when I click on any Button, I can see on Chrome React Tools that the value for isSelected of that button becomes true. I can also confirm that the specific array item for the button clicked in the dev tools for State (under hooks), the value is true.
The text for the button clicked does not show [ One true ] say if I clicked on button One. What am I missing here?
P.S. Note that I also want to change the class of the button, but I think that part will be resolved if I get the button isSelected value to be known across the component.
Code Demo:
https://codesandbox.io/s/laughing-keller-o5mds?file=/src/App.js:666-735
Issue: You are mutating the state object instead of returning a new state reference. You were also previously using === to compare a string id to a numerical id which was returning false for all comparisons.
Solution: Use a functional update and array.map to update state by returning a new array.
const ButtonItem = ({ item }) => {
const btnClick = event => {
const id = event.target.value;
setButtonList(buttons =>
buttons.map(button => ({
...button,
isSelected: button.id == id
}))
);
};
...
};
Suggestion: Factor out the btnClick handler, it only needs to be defined once. Curry the id property of item so you can use ===.
const btnClick = id => event => {
setButtonList(buttons =>
buttons.map(button => ({
...button,
isSelected: button.id === id
}))
);
};
Update the attaching of click handler to pass the item id
const ButtonItem = ({ item }) => {
return (
<button
type="button"
className={
"btn mx-2 " +
(item.isSelected ? "btn-primary" : "btn-outline-primary")
}
onClick={btnClick(item.id)} // <-- pass item.id to handler
value={item.id}
>
{item.name + " " + item.isSelected}
</button>
);
};
In your btnClick handler you are mutating your state, you should create a new value and assign it instead:
import React from "react";
import "./styles.css";
import { useState, useEffect } from "react";
const ButtonItem = ({ item, onClick }) => {
return (
<button
type="button"
className={
"btn mx-2 " + (item.isSelected ? "btn-primary" : "btn-outline-primary")
}
onClick={() => onClick(item.id)}
value={item.id}
>
{item.name + " " + item.isSelected}
</button>
);
};
const MyComponent = props => {
const [buttonList, setButtonList] = useState([]);
useEffect(() => {
getButtonList();
}, []);
const getButtonList = () => {
let data = [
{ id: 1, name: "One", isSelected: false },
{ id: 2, name: "Two", isSelected: false },
{ id: 3, name: "Three", isSelected: false }
];
setButtonList(data);
};
const btnClick = id => {
const updatedList = buttonList.map(el => ({
...el,
isSelected: el.id === id
}));
setButtonList(updatedList);
console.log("buttonList", updatedList);
};
return (
<div className="container-fluid">
<div className="card mb-3 rounded-lg">
<div className="card-body">
{buttonList.map(item => (
<ButtonItem key={item.id} item={item} onClick={btnClick} />
))}
</div>
</div>
</div>
);
};
export default MyComponent;
I have a list of objects that I'd like to pass to a map function which passes each object as props to a component to be rendered.
I have a menu and clicking each item calls setActiveItem() updating activeItem which is being managed by useState hook.
I'm trying to filter the list of objects based on this activeItem value. I've created a base case trying to replicate the problem but my base case works flawlessly though it'll at least clarify what I'm trying to do so here it is:
import React, { useState } from 'react';
import { Menu } from 'semantic-ui-react';
const [ALL, NUMBER, LETTER] = ['All', 'Number', 'Letter'];
const data = [
{
tags: [ALL, NUMBER],
value: '1'
},
{
tags: [ALL, LETTER],
value: 'a'
},
{
tags: [ALL, NUMBER],
value: '2'
},
{
tags: [ALL, LETTER],
value: 'b'
},
{
tags: [ALL, NUMBER],
value: '3'
},
{
tags: [ALL, LETTER],
value: 'c'
},
{
tags: [ALL, NUMBER],
value: '4'
},
{
tags: [ALL, LETTER],
value: 'd'
}
];
const renderData = (allValues, filterTag) => {
let filteredList = allValues.filter(val => {
return val['tags'].includes(filterTag);
});
return (
<div>
{filteredList.map(object_ => {
return object_.value;
})}
</div>
);
};
const BaseCase = props => {
const [activeItem, setActiveItem] = useState(ALL);
return (
<div>
<Menu inverted stackable fluid widths={4}>
<Menu.Item
name={ALL}
active={activeItem === ALL}
onClick={(e, { name }) => setActiveItem(name)}
/>
<Menu.Item
name={NUMBER}
active={activeItem === NUMBER}
onClick={(e, { name }) => setActiveItem(name)}
/>
<Menu.Item
name={LETTER}
active={activeItem === LETTER}
onClick={(e, { name }) => setActiveItem(name)}
/>
</Menu>
<div>{renderData(data, activeItem)}</div>
</div>
);
};
export default BaseCase;
Clicking number only shows numbers and everything else works as expected. Now for my component that isn't working. I have my data in a separate file like so:
import { BASH, DATA_SCIENCE, WEB_DEV, ALL } from '../constants';
const data = [
{
tags: [ALL],
title: 'Concussion App for Athletes',
.
.
.
},
{
tags: [DATA_SCIENCE, ALL],
title: 'Deep Learning: Exploring Car Value with an ANN',
...
},
.
.
.
];
export default data;
Here's my component. There's some commented out code that I tried but that also gave incorrect components being displayed.
import React, { useState } from 'react';
import ProjectCardContainer from '../../containers/ProjectCardContainer';
import { Menu } from 'semantic-ui-react';
import { ALL, BASH, DATA_SCIENCE, WEB_DEV } from './constants';
import data from './project_data';
import './Projects.scss';
const styles = {
container: {
display: 'flex',
justifyContent: 'space-around'
},
columns: {
display: 'flex',
flexDirection: 'column',
marginTop: '11px'
}
};
const renderColumn = (projectList, filterTag) => {
let projects = projectList.filter(proj => {
return proj['tags'].includes(filterTag);
});
return (
<div style={styles.columns}>
{projects.map(project => {
return <ProjectCardContainer project={project} />;
})}
</div>
);
};
const Projects = () => {
const [activeItem, setActiveItem] = useState(ALL);
// const [, updateState] = React.useState();
// const forceUpdate = useCallback(() => updateState({}), []);
// useEffect(() => {
// setTimeout(forceUpdate, 100);
// }, [activeItem]);
return (
<div>
<div className='second-nav-container'>
<Menu inverted stackable fluid widths={4}>
<Menu.Item
name={ALL}
active={activeItem === ALL}
onClick={(e, { name }) => setActiveItem(name)}
/>
<Menu.Item
name={WEB_DEV}
active={activeItem === WEB_DEV}
onClick={(e, { name }) => setActiveItem(name)}
/>
<Menu.Item
name={DATA_SCIENCE}
active={activeItem === DATA_SCIENCE}
onClick={(e, { name }) => setActiveItem(name)}
/>
<Menu.Item
name={BASH}
active={activeItem === BASH}
onClick={(e, { name }) => setActiveItem(name)}
/>
</Menu>
</div>
<div style={styles.container}>{renderColumn(data, activeItem)}</div>
</div>
);
};
export default Projects;
Basically the rendered list of components usually isn't correct except maybe when the page is refreshed and the default value from useState() is used. Selecting from the menu doesn't show the components of the correct category.
I believe the problem is that the render function is getting called before activeItem is updated but I'm not sure how to work around that issue. I'm somewhat new to using hooks but this seems like a problem that must come up a lot.
Anyone Have any ideas how I can use a menu like this to filter data then only show specific components based on filtered data?
The problem in the end was I wasn't providing a unique key while rendering lists of components. The solution is to provide a unique key like so:
const renderColumn = (projectList, filterTag) => {
let projects = projectList.filter(proj => {
return proj['tags'].includes(filterTag);
});
return (
<div style={styles.columns}>
{projects.map(project => {
return <ProjectCardContainer key={project.title} project={project} />;
})}
</div>
);
};
In my case I know the titles will be unique so this works.
I don't think we need to mess around too much with complicated state management. I updated the base case to meet your needs:
Constants.js:
export const [ALL, DATA_SCIENCE, WEB_DEV, BASH] = ['All', 'DATA_SCIENCE', 'WEB_DEV', 'BASH'];
data.js:
import {ALL, DATA_SCIENCE, WEB_DEV, BASH} from './Constants';
const data = [
{
tags: [ALL],
title: 'Concussion App for Athletes',
},
{
tags: [DATA_SCIENCE, ALL],
title: 'Deep Learning: Exploring Car Value with an ANN',
},
{
tags: [BASH, ALL],
title: 'Bash 101'
},
{
tags: [WEB_DEV, ALL],
title: 'Web Development Book'
},
{
tags: [WEB_DEV, ALL],
title: 'Fundamentals of web design'
}
]
export default {data};
BaseCase.js:
import React, { useState } from 'react';
import { Menu } from 'semantic-ui-react';
import data from './data';
import {ALL, DATA_SCIENCE, WEB_DEV, BASH} from './Constants';
const renderData = (allValues, filterTag) => {
let filteredList = Object.values(allValues.data).filter(val => {
return val['tags'].includes(filterTag);
});
return (
<div>
{filteredList.map(object_ => {
return <p>{object_.title}</p>;
})}
</div>
);
};
const BaseCase = props => {
const [activeItem, setActiveItem] = useState(ALL);
const newData = data;
return (
<div>
<Menu inverted stackable fluid widths={4}>
<Menu.Item
name={ALL}
active={activeItem === ALL}
onClick={(e, { name }) => setActiveItem(name)}
/>
<Menu.Item
name={DATA_SCIENCE}
active={activeItem === DATA_SCIENCE}
onClick={(e, { name }) => setActiveItem(name)}
/>
<Menu.Item
name={WEB_DEV}
active={activeItem === WEB_DEV}
onClick={(e, { name }) => setActiveItem(name)}
/>
<Menu.Item
name={BASH}
active={activeItem === BASH}
onClick={(e, { name }) => setActiveItem(name)}
/>
</Menu>
<div>{renderData(newData, activeItem)}</div>
</div>
);
};
export default BaseCase;
At return <p>{object_.title}</p>; render out your component like <ProjectCardContainer project={object_} />
Trying to figure out how to transfer data between siblings components. The idea is this: you need to make sure that only one child component has an "active" class (Only one div could be selected). Here is the code:
https://codepen.io/slava4ka/pen/rNBoJGp
import React, {useState, useEffect} from 'react';
import styleFromCss from './Garbage.module.css';
const ChildComponent = (props) => {
const [style, setStyle] = useState(`${styleFromCss.childComponent}`);
const [active, setActive] = useState(false)
const setActiveStyle = () => {
console.log("setActiveStyle");
if (!active) {
setStyle(`${styleFromCss.childComponent} ${styleFromCss.active}`)
setActive(true)
} else {
setStyle(`${styleFromCss.childComponent}`);
setActive(false)
}
};
//console.log(props);
console.log(`id ${props.id} style ${style}`);
return (
<div className={style} onClick={() => {
props.updateData(props.id, () => setActiveStyle())
}}>
<h3>{props.name}</h3>
</div>
)
};
const ParentComponent = (props) => {
const state = [{'id': 0, 'name': 'один'}, {'id': 1, 'name': 'два'}, {'id': 2, 'name': 'три'}];
const [clicked, setClicked] = useState(null);
const highlight = (id, makeChildActive) => {
//console.log("click! " + id);
setClicked(id);
makeChildActive();
};
return (
<div>
{state.map(entity => <ChildComponent updateData={highlight}
key={entity.id}
id={entity.id}
name={entity.name}
isActive={(entity.id ===
clicked) ? styleFromCss.active : null}
/>)}
</div>
)
};
export default ParentComponent;
styleFromCss.module.css:
.childComponent{
width: 200px;
height: 200px;
border: 1px solid black;
margin: 10px;
float: left;
text-align: center;
}
.active{
background-color: blueviolet;
}
I tried to implement this through hooks, not classes. As a result, when you click on the selected component, its classes change, and nothing happens on its siblings. As I understand it, they simply do not redraw. The question is how to fix it? And is such an approach correct for the realization of a given goal? Or my idea is fundamentally wrong, I will be grateful for the advice)))
You cannot use the Child's onClick method to set the style as when one Child is clicked, the other Children don't know that :((
Instead, when you click on one Child, it tells the Parent it is clicked (you do this correctly already with setClicked()), then the Parent can tell each Child whether they are active or not (by passing IsActive boolean), and each Child uses its props.isActive boolean to set its style :D
const ChildComponent = (props) => {
let style = 'childComponent'
if (props.isActive) style = style + ' active'
return (
<div className={style} onClick={() => {
props.updateData(props.id)
}}>
<h3>{props.name}</h3>
</div>
)
};
const ParentComponent = (props) => {
const state = [{'id': 0, 'name': 'один'}, {'id': 1, 'name': 'два'}, {'id': 2, 'name': 'три'}];
const [clicked, setClicked] = React.useState(null);
const highlight = (id) => {
setClicked(id);
};
return (
<div>
{state.map(entity =>
<ChildComponent updateData={highlight}
key={entity.id}
id={entity.id}
name={entity.name}
isActive={entity.id === clicked}
/>
)}
</div>
)
};
ReactDOM.render(
<ParentComponent/>,
document.getElementById('root')
);
You need to pass a function down into the child from the parent that handles state change in the parent component for a piece of state that determines which id is "active", lets call that activeId. Then pass activeId into the child as a prop. In the child, compare the id to the activeId and apply the class accordingly.
Correct, this is the wrong way to be thinking about it. If you only want one child to be selected at a time, you should keep the "which child is selected" data in the parent. Each child should not also manage its own version of its highlighted state; instead it should be a prop given to each child (each child is a pure function.) Try this:
const ChildComponent = props => {
return (
<div
className={'childComponent' + (props.isActive ? ' active' : '')}
onClick={() => props.setHighlighted(props.id)}
>
<h3>{props.name}</h3>
</div>
);
};
const ParentComponent = props => {
const state = [
{ id: 0, name: 'один' },
{ id: 1, name: 'два' },
{ id: 2, name: 'три' }
];
const [clicked, setClicked] = React.useState(null);
return (
<div>
{state.map(entity => (
<ChildComponent
setHighlighted={setClicked}
key={entity.id}
id={entity.id}
name={entity.name}
isActive={entity.id === clicked ? 'active' : null}
/>
))}
</div>
);
};
ReactDOM.render(<ParentComponent />, document.getElementById('root'));