React - How To Compare Props Between Separate Components - javascript

How can I compare if the props between two separate components have the same value?
1- Is what I'm seeking doable?
2- If not, how else could I accomplish the ask below:
The story:
I have an array of car objects.
Each car's name is displayed as <li /> on a <CarList /> component.
Upon click on each <li/> the car's color is revealed
I have a <Question /> component that renders: "What car is (random color here)"?
UI change:
How could I write a method that:
Checks if the props.color of <CarList /> === the props.color of <Question />
Then it fires a UI change such as:
onClick: If the car's color matches the question's color: change the <li /> to green (ie: background-color), else change it to red.
I'm struggling (wondering if it's possible) to compare props from different components + writing a method that checks and executes the UI change above.
This is the code reflecting the explanation above: Also here's the sandbox
// Garage
export default class Garage extends Component {
state = {
cars: [
{ name: "Ferrari", color: "red", id: 1 },
{ name: "Porsche", color: "black", id: 2 },
{ name: "lamborghini", color: "green", id: 3 },
{ name: "McLaren", color: "silver", id: 4 },
{ name: "Tesla", color: "yellow", id: 5 }
]
};
handleShuffle = () => {
this.setState({
cars: [...this.state.cars.sort(() => Math.random() - 0.5)]
});
};
render() {
const { cars } = this.state;
const car = cars.map(car => (
<CarList key={car.id} make={car.name} color={car.color} />
));
const guess = cars
.slice(2, 3)
.map(car => <Question key={car.id} color={car.color} />);
return (
<>
<div>{guess}</div>
<button onClick={this.handleShuffle}>load color</button>
<ul>{car}</ul>
</>
);
}
}
// CarList
class CarList extends Component {
state = {
show: false
};
handleShow = () => {
this.setState({ show: true });
console.log(this.props);
// check for props equality here
//desired result for <li /> would be
// className={ correctColor ? 'green' : 'red'}
};
render() {
console.log("car color props:", this.props.color);
const { make, color } = this.props;
const { show } = this.state;
return (
<li onClick={this.handleShow}>
{make}
<span className={show ? "show" : "hide"}>{color}</span>
</li>
);
}
}
// Question
const Question = ({ color }) =>
console.log("question color prop:", color) || <h1>What car is {color}</h1>;

Yes, you can pass the correct color to the CarList component or the flag whether the CarList is a correct one. Check my sandbox.
https://codesandbox.io/s/92xnwpyq6p
Basically we can add isCorrect prop to CarList which has value of correctCar.color === car.color and we use it to determine whether we should render it green or red.

Theres many ways to do this but the simplest is to send the color in the question down to the car component.
https://codesandbox.io/s/my4wmn427x

Related

React component function call only updates one component instance

I have a component called RightTab like this
const RightTab = ({ data }) => {
return (
<div className="RightTab flex__container " onClick={data.onClick}>
<img src={data.icon} alt="Dashboard Icon" />
<p className="p__poppins">{data.name}</p>
{data.dropDown === true ? (
<div className="dropdown__icon">
<img src={Assets.Arrow} alt="Arrow" />
</div>
) : (
<div className="nothing"></div>
)}
</div>
);
};
export default RightTab;
The tab has an active state in its CSS like this
.RightTab.active {
background-color: var(--primaryGreen);
}
as you have seen it changes the color when an active class is added. I have an array in the parent component that I pass down to the child component as props. Here is the array
const dataArray = [
{
name: "Dashboard",
icon: Assets.Dashboard,
dropDown: false,
onClick: handleDashBoardClick,
},
{
name: "Inventory",
icon: Assets.Inventory,
dropDown: true,
onClick: handleInventoryClick,
},
{
name: "Reports",
icon: Assets.Reports,
dropDown: true,
onClick: handleReportsClick,
},
];
Here is how I pass the props down.
<RightTab data={dataArray[0]} />
<RightTab data={dataArray[1]} />
<RightTab data={dataArray[2]} />
The data prop passed into the component is an object containing a function call as one of its properties like this. I have an onclick attribute on the child components' main container that is supposed to call the respective function.
The function is what adds the active class to make the background change color. However each time I click on the component it only changes the background of the first occurrence. And as you may have noticed I call the component thrice. No matter which component I click only the first ones background changes.
Here is an example of the function that is on the prop object.
const handleDashBoardClick = () => {
const element = document.querySelector(".RightTab");
element.classList.toggle("active");
};
I don't get what I'm doing wrong. What other approach can I use?
Although you use the component 3 times, it doesn't mean that a change you make in one of the components will be reflected in the other 2, unless you specifically use a state parameter that is passed to all 3 of them.
Also, the way you add the active class is not recommended since you mix react with pure js to handle the CSS class names.
I would recommend having a single click handler that toggles the active class for all n RightTab components:
const MainComponent = () => {
const [classNames, setClassNames] = useState([]);
const handleClick = (name) =>
{
const toggledActiveClass = classNames.indexOf('active') === -1
? classNames.concat(['active'])
: classNames.filter((className) => className !== 'active');
setClassNames(toggledActiveClass);
switch (name) {
case 'Dashboard';
// do something
break;
case 'Inventory':
// ....
break;
}
}
const dataArray = [
{
name: "Dashboard",
icon: Assets.Dashboard,
dropDown: false,
onClick: handleClick.bind(null, 'Dashboard'),
},
{
name: "Inventory",
icon: Assets.Inventory,
dropDown: true,
onClick: handleClick.bind(null, 'Inventory'),
},
{
name: "Reports",
icon: Assets.Reports,
dropDown: true,
onClick: handleClick.bind(null, 'Reports'),
},
];
return (
<>
{dataArray.map((data) =>
<RightTab key={data.name}
data={data}
classNames={classNames} />)}
</>
);
};
const RightTab = ({ data, classNames }) => {
return (
<div className={classNames.concat(['RightTab flex__container']).join(' ')}
onClick={data.onClick}>
<img src={data.icon} alt="Dashboard Icon" />
<p className="p__poppins">{data.name}</p>
{data.dropDown === true ? (
<div className="dropdown__icon">
<img src={Assets.Arrow} alt="Arrow" />
</div>
) : (
<div className="nothing"></div>
)}
</div>
);
};

ReactJS How to use Refs on components rendered dinamically by another render function to focus elements?

I have a class component that Renders a list of elements and I need to focus them when an event occurs.
Here is an example code
class page extends React.Component {
state = {
items: [array of objects]
}
renderList = () => {
return this.state.items.map(i => <button>{i.somekey}</button>)
}
focusElement = (someitem) => {
//Focus some item rendered by renderList()
}
render(){
return(
<div>
{this.renderList()}
<button onClick={() => focusElement(thatElement)}>
</div>
)
}
}
I know that I need to use refs but I tried several ways to do that and I couldn't set those refs properly.
Can someone help me?
you should use the createRefmethod of each button that you would like to focus, also you have to pass this ref to the focusElement method that you have created:
const myList = [
{ id: 0, label: "label0" },
{ id: 1, label: "label1" },
{ id: 2, label: "label2" },
{ id: 3, label: "label3" },
{ id: 4, label: "label4" },
{ id: 5, label: "label5" }
];
export default class App extends React.Component {
state = {
items: myList,
//This is the list of refs that will help you pick any item that ou want to focus
myButtonsRef: myList.map(i => React.createRef(i.label))
};
// Here you create a ref for each button
renderList = () => {
return this.state.items.map(i => (
<button key={i.id} ref={this.state.myButtonsRef[i.id]}>
{i.label}
</button>
));
};
//Here you pass the ref as an argument and just focus it
focusElement = item => {
item.current.focus();
};
render() {
return (
<div>
{this.renderList()}
<button
onClick={() => {
//Here you are able to focus any item that you want based on the ref in the state
this.focusElement(this.state.myButtonsRef[0]);
}}
>
Focus the item 0
</button>
</div>
);
}
}
Here is a sandbox if you want to play with the code

Rendering the navigation list from an array based on different label on toggle mode

I have a header component where I need to render three buttons, so every three buttons have three props. One is the class name, click handler and text.
So out of three buttons, two buttons act as a toggle button, so based on the click the text should change.
See the below code:
class App extends Component(){
state = {
navigationList: [{
text: 'Signout',
onClickHandler: this.signoutHandler,
customClassName: 'buttonStyle'
}, {
text: this.state.isStudents ? 'Students' : 'Teachers',
onClickHandler: this.viewMode,
customClassName: 'buttonStyle'
}, {
text: this.state.activeWay ? 'Active On' : 'Active Hidden',
onClickHandler: this.activeWay,
customClassName: 'buttonStyle'
}]
}
signoutHandler = () => {
// some functionality
}
viewMode = () => {
this.setState({
isStudents: !this.state.isStudents
})
}
activeWay = () => {
this.setState({
activeWay: !this.state.activeWay
})
}
render(){
return (
<Header navigationList={this.state.navigationList}/>
)
}
}
const Header = ({navigationList}) => {
return (
<>
{navigationList && navigationList.map(({text, onClickHandler, customClassName}) => {
return(
<button
onClick={onClickHandler}
className={customClassName}
>
{text}
</button>
)
})}
</>
)
}
The other way is I can pass all the props one by one and instead of an array I can write three button elements render it, but I am thinking to have an array and render using a map.
So which method is better, the problem that I am facing is if use the array. map render
the approach I need to set the initial value as a variable outside and how can I set the state.
And I am getting the onClick method is undefined, is it because the function is not attached to the state navigation list array.
Update
I declared the functions above the state so it was able to call the function.
So in JS, before the state is declared in the memory the functions should be hoisted isn't.
class App extends React.Component {
constructor(props){
super();
this.state = {
isStudents:false,
activeWay:false,
}
}
createList(){
return [{
text: 'Signout',
onClickHandler: this.signoutHandler.bind(this),
customClassName: 'buttonStyle'
}, {
text: this.state.isStudents ? 'Students' : 'Teachers',
onClickHandler: this.viewMode.bind(this),
customClassName: 'buttonStyle'
}, {
text: this.state.activeWay ? 'Active On' : 'Active Hidden',
onClickHandler: this.activeWay.bind(this),
customClassName: 'buttonStyle'
}];
}
signoutHandler(){
}
viewMode(){
this.setState({
isStudents: !this.state.isStudents
})
}
activeWay(){
this.setState({
activeWay: !this.state.activeWay
})
}
render(){
return (
<div>
<div>ddd</div>
<Header navigationList={this.createList()} />
</div>
)
}
}
const Header = ({navigationList}) => {
console.log(navigationList);
return (
<div>
{navigationList && navigationList.map(({text, onClickHandler, customClassName}) => {
return(
<button
onClick={onClickHandler}
className={customClassName}
>
{text}
</button>
)
})}
</div>
)
}
ReactDOM.render(<App />, document.querySelector("#app"))
https://jsfiddle.net/luk17/en9h1bpr/
Ok I will try to explain, If you see you are using function expressions in your class and as far as hoisting is concerned in JavaScript, functions expressions are not hoisted in JS only function declarations are hoisted, function expressions are treated as variables in JS.
Now for your case you don't have to shift your functions above the state, you can simply use constructor for initializing state as
constructor(props) {
super(props);
this.state = {
isStudents: false,
activeWay: false,
navigationList: [
{
text: "Signout",
onClickHandler: this.signoutHandler,
customClassName: "buttonStyle"
},
{
text: "Teachers",
onClickHandler: this.viewMode,
customClassName: "buttonStyle"
},
{
text: "Active Hidden",
onClickHandler: this.activeWay,
customClassName: "buttonStyle"
}
]
};
}
Now you will have your handlers available as it is
Sandbox with some modification just to show
EDIT:
You can have default text for buttons and change it when clicking,
Sandbox updated
Hope it helps

What do I need to change in toggle function to show 1 element at a time rather than show all elements?

I'm building a dog breed info application and when I click on the breed name I want to show the info for that breed only. Currently, the toggle function does show the dog breed info when the breed name is clicked but it shows the info for all breeds rather than just the breed clicked.
I think the problem is that I'm not calling the object id correctly but I can't figure out where the id needs to be called. What am I missing? Thanks!
class Dog extends React.Component {
constructor(props) {
super(props);
this.state = {
dogInfo: [
{
id: 1,
key: 'dogBreed',
breedName: 'Tibetan Mastiff',
class: 'Working',
colors: ['Black', 'Black & Tan', 'Blue Gray', 'Blue Gray & Tan', 'Brown', 'Brown & Tan', 'Red Gold', 'Red Gold Sable'],
image: tibetanMastiff,
alt: 'Black and Tan Tibetan Mastiff'
},
{
id: 2,
key: 'dogBreed',
breedName: 'Great Dane',
class: 'Working',
colors: ['Black', 'Black & White', 'Blue', 'Brindle', 'Fawn', 'Harlequin', 'Mantle', 'Merle', 'White'],
image: greatDane,
alt: 'Merle Great Dane'
},
{
id: 3,
key: 'dogBreed',
breedName: 'Cavalier King Charles Spaniel',
class: 'Toy',
colors: ['Blenheim', 'Black & Tan', 'Tri-Color', 'Ruby'],
image: cavalier,
alt: 'Tri-Color Cavalier King Charles Spaniel'
},
{
id: 4,
key: 'dogBreed',
breedName: 'Italian Greyhound',
class: 'Toy',
colors: ['Black', 'Blue', 'Blue Fawn', 'Cream', 'Fawn', 'Red', 'Red Fawn', 'Sable', 'Seal'],
image: italianGrayhound,
alt: 'Fawn Italian Grayhound'
}
]
}
}
toggleSelected(id, key){
let temp = this.state[key]
temp[id].selected = !temp[id].selected
this.setState({
[key]: temp
})
}
render() {
return (
<div className="App">
<div className='wrapper'>
<DogList
title='Choose Dog Breed'
breedInfo={this.state.dogInfo}
toggleItem={this.toggleSelected}
/>
</div>
</div>
);
}
}
export default Dog;
class DogList extends React.Component {
constructor(props) {
super(props);
this.state = {
listOpen: false,
headerTitle: this.props.title
}
}
toggleList(){
this.setState(prevState => ({
listOpen: !prevState.listOpen
}))
}
render(){
const{breedInfo} = this.props
const{listOpen, headerTitle} = this.state
return(
<div>
<div >{headerTitle}</div>
{<ul onClick={() => this.toggleList()}>
{breedInfo.map((dog) => (
<li key={dog.id} >{dog.breedName}
{listOpen && <ul onClick={() => this.toggleList()}>
<img src={dog.image}/>
<li key={dog.id} >{dog.colors.map((color) => (
<ul>
<li>{color}</li>
</ul>
))}</li>
</ul>}
</li>
))}
</ul>}
</div>
)
}
}
export default DogList;
Hello smallDisgruntledDog, great name by the way. Taking a look at your code, there are a variety of ways to accomplish what you're looking for. As currently constructed, it looks like you are trying to make use of your toggleList()function to display one dog-breed at a time. However, the listOpen value is shared across all of your dogs, so it ends up opening all lists. There is no unique value being passed to determine which dog was selected.
I have 2 ways for you to solve this.
Option 1) Add an extra state value in your DogList component called selectedDog. Update your toggleList function to take in a dog breed value, which we will use to update the selectedDog state. Now in order to display a dog list, both listOpen has to be true and the selectedDog must match dog.breedName:
import React from "react";
class DogList extends React.Component {
constructor(props) {
super(props);
this.state = {
listOpen: false,
headerTitle: this.props.title,
selectedDog: null
};
}
toggleList(dogBreed) {
this.setState(prevState => ({
listOpen: !prevState.listOpen,
selectedDog: dogBreed
}));
}
render() {
const { breedInfo } = this.props;
const { listOpen, headerTitle, selectedDog } = this.state;
return (
<div>
<div>{headerTitle}</div>
{
<ul>
{breedInfo.map(dog => (
<li key={dog.id} onClick={() => this.toggleList(dog.breedName)}>
{dog.breedName}
{listOpen && selectedDog === dog.breedName && (
<ul>
<img src={dog.image} />
<li key={dog.id}>
{dog.colors.map(color => (
<ul>
<li>{color}</li>
</ul>
))}
</li>
</ul>
)}
</li>
))}
</ul>
}
</div>
);
}
}
export default DogList;
Option 2) Separate each Dog into a different component. I'm going to display this using a component called DogBreed. There are many benefits to doing this including writing cleaner code, giving each dog their own state and giving us more control over each dog.
DogBreed component:
import React from "react";
class DogBreed extends React.Component {
state = {
show: false
};
handleClick = () => {
this.setState(prevState => {
return {
show: !prevState.show
};
});
};
render() {
const { show } = this.state;
return (
<li onClick={this.handleClick}>
{this.props.breedName}
{show ? (
<ul>
<img src={this.props.image} />
<li>
{this.props.colors.map(color => (
<ul>
<li>{color}</li>
</ul>
))}
</li>
</ul>
) : null}
</li>
);
}
}
export default DogBreed;
Updated DogList component:
import React from "react";
import DogBreed from "./DogBreed";
class DogList extends React.Component {
constructor(props) {
super(props);
this.state = {
headerTitle: this.props.title
};
}
toggleList() {
this.setState(prevState => ({
listOpen: !prevState.listOpen
}));
}
render() {
const { breedInfo } = this.props;
const { headerTitle } = this.state;
return (
<div>
<div>{headerTitle}</div>
{
<ul>
{breedInfo.map(dog => (
<DogBreed
breedName={dog.breedName}
colors={dog.colors}
image={dog.image}
/>
))}
</ul>
}
</div>
);
}
}
export default DogList;
Let me know if you have any questions. For reference, here are the codesandboxes for both options:
Option 1: https://codesandbox.io/s/1x82qq82j
Option 2: https://codesandbox.io/s/v3062w69v0

Handle Input with Same State Value

I'm building a shopping cart application and I ran into a problem where all my inputs have the same state value. Everything works fine but when I type in one input box, it's the same throughout all my other inputs.
I tried adding a name field to the input and setting my initial state to undefined and that works fine but the numbers don't go through.
How do we handle inputs to be different when they have the same state value? Or is this not possible / dumb to do?
class App extends Component {
state = {
items: {
1: {
id: 1, name: 'Yeezys', price: 300, remaining: 5
},
2: {
id: 2, name: 'Github Sweater', price: 50, remaining: 5
},
3: {
id: 3, name: 'Protein Powder', price: 30, remaining: 5
}
},
itemQuantity: 0
},
render() {
return (
<div>
<h1>Shopping Area</h1>
{Object.values(items).map(item => (
<div key={item.id}>
<h2>{item.name}</h2>
<h2>$ {item.price}</h2>
{item.remaining === 0 ? (
<p style={{ 'color': 'red' }}>Sold Out</p>
) : (
<div>
<p>Remaining: {item.remaining}</p>
<input
type="number"
value={ itemQuantity }
onChange={e => this.setState({ itemQuantity: e.target.value})}
placeholder="quantity"
min={1}
max={5}
/>
<button onClick={() => this.addItem(item)}>Add To Cart</button>
</div>
)}
</div>
))}
</div>
)
}
}
If you are using same state key for all input, All input take value from one place and update to one place. To avoid this you have to use separate state. I suppose you are trying to show input for a list of item.
To achive you can create a component for list item and keep state in list item component. As each component have their own state, state value will not conflict.
Here is an example
class CardItem extends Component {
state = {
number: 0
}
render() {
render (
<input type="text" value={this.state.number} onChange={e => this.setState({ number: e.target.value })} />
)
}
}
class Main extends Component {
render () {
const list = [0,1,2,3,4]
return (
list.map(item => <CardItem data={item} />)
)
}
}
This is a solution which the problem is loosely interpreted, but it does work without having to create another component. As you know, you needed to separate the state of each items in the cart. I did this by dynamically initializing and setting the quantity states of each item. You can see the state changes with this example:
class App extends React.Component {
constructor(props) {
super(props);
this.state = { quantities: {} }
}
componentDidMount() {
let itemIDs = ['1', '2', '3', 'XX']; //use your own list of items
itemIDs.forEach(id => {
this.setState({quantities: Object.assign(this.state.quantities, {[id]: 0})});
})
}
render() {
let list = Object.keys(this.state.quantities).map(id => {
return (
<div>
<label for={id}>Item {id}</label>
<input
id={id}
key={id}
type="number"
value={this.state.quantities[id]}
onChange={e => {
this.setState({quantities: Object.assign(this.state.quantities, {[id]: e.target.value})})
}}
/>
</div>
);
})
return (
<div>
{list}
<div>STATE: {JSON.stringify(this.state)}</div>
</div>
);
}
}
ReactDOM.render(<App/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id='root'></div>
You can modify the state structure to your liking.
Here is how I usually handle this scenario. You say that you get an array of items? Each item object should contain a key to store the value (count in my example). You can use a generic onChange handler to update an individual item in the array. So now, your state is managing the list of items instead of each individual input value. This makes your component much more flexible and it will be able to handle any amount of items with no code changes:
const itemData = [
{ id: 0, count: 0, label: 'Number 1' },
{ id: 1, count: 0, label: 'Number 2' },
{ id: 2, count: 0, label: 'Number 3' },
{ id: 3, count: 0, label: 'Number 4' }
];
class App extends React.Component {
state = {
items: itemData
}
handleCountChange = (itemId, e) => {
// Get value from input
const count = e.target.value;
this.setState( prevState => ({
items: prevState.items.map( item => {
// Find matching item by id
if(item.id === itemId) {
// Update item count based on input value
item.count = count;
}
return item;
})
}))
};
renderItems = () => {
// Map through all items and render inputs
return this.state.items.map( item => (
<label key={item.label}>
{item.label}:
<input
type="number"
value={item.count}
onChange={this.handleCountChange.bind(this, item.id)}
/>
</label>
));
};
render() {
return (
<div>
{this.renderItems()}
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'));
label {
display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
You can't use the same state for the both inputs. Try to use a different state for each one like that:
class App extends Component {
state = {
number: ""
}
render() {
return (
<div>
<input
type="number"
value={this.state.number}
onChange={e => this.setState({ number: e.target.value })}
/>
<input
type="number"
value={this.state.number2}
onChange={e => this.setState({ number2: e.target.value })}
/>
</div>
)
}
}

Categories