React - interview exercise - javascript

I got the following React exercise with 2 components in an interview that I did not manage to make it compile...
The question was the following:
Update the Counter component to take onIncrement callbacks as props and ensure they update the counter's values independently. Each callback should take a single, integer value as a parameter which is the amount to increment the counter's existing value by.
Comments in the code but the my problem is how to implement the "onIncrement" function.
const { Component } = React;
const { render } = ReactDOM;
// state data for 3 counters
const data = [
{ id: 1, value: 1 },
{ id: 2, value: 2 },
{ id: 3, value: 3 }
];
// Counter Component
class Counter extends Component {
render() {
const { value } = this.props;
return (
<div className="counter">
<b>{value}</b>
<div className="counter-controls">
<button className="button is-danger is-small">-</button>
//I call the function passed
<button className="button is-success is-small" onClick={()=>{onIncrement(this.props.value)}}>+</button>
</div>
</div>
);
}
}
class App extends Component {
constructor(props, context) {
super(props, context);
}
onIncrement=(value)=>{
//I tried several things here but I did not manage to make it work. I guess that I need also the id of the object...
}
render() {
return (
<div>
{data.map(counter => (
<Counter
key={counter.id}
value={counter.value}
//I pass the callback function to the component
onIncrement={this.onIncrement}
/>
))}
</div>
);
}
}
render(
<App/>
, document.querySelector('#root'))

Basically, you'll want to use the id as a way to determine which value you need to update. How you have it set up, you won't be able to know which value needs to be updated (because you don't know which id was clicked) nor will the value be saved.
NOTE: The example below takes the id from event.target.id and the value from event.target.value which is then deconstructed in the handleChange callback. This is a more common and elegant solution than passing a value to a callback and then passing it and another value to another callback (more work, more code, but same functionality).
Best solution: https://codesandbox.io/s/rjmx8vw99p
components/UpdateQuantity.js
import React, { Component, Fragment } from "react";
export default class App extends Component {
state = {
items: [
{ id: "Apples", quantity: 0 },
{ id: "Strawberries", quantity: 0 },
{ id: "Grapes", quantity: 0 },
{ id: "Apricots", quantity: 0 }
]
};
handleChange = ({ target: { id, value } }) => {
this.setState(prevState => ({
items: prevState.items.map(item => {
const nextVal = item.quantity + ~~value; // ~~ === parseInt(val, 10) -- required because the "value" is turned into a string when placed on a DOM element
return id === item.id
? { id, quantity: nextVal > 0 ? nextVal : 0 }
: { ...item };
})
}));
};
render = () => (
<div className="container">
<h1>Updating Values Inside Array</h1>
{this.state.items.map(({ id, quantity }) => (
<div key={id} className="container">
<div>
{id} ({quantity})
</div>
<button
id={id}
value={1}
style={{ marginRight: 10 }}
className="uk-button uk-button-primary"
onClick={this.handleChange}
>
+
</button>
<button
id={id}
value={-1}
style={{ marginRight: 10 }}
className="uk-button uk-button-danger"
onClick={this.handleChange}
>
-
</button>
</div>
))}
</div>
);
}
Another solution: https://codesandbox.io/s/yq961275rv (not recommended as it requires an extra component and an extra callback -- BUT there's no binding required in the render method nor is there an anonymous function () => {} in the onClick callback)
components/UpdateQuantity.js
import React, { Component, Fragment } from "react";
import Button from "./button";
export default class App extends Component {
state = {
items: [
{ id: "Apples", quantity: 0 },
{ id: "Strawberries", quantity: 0 },
{ id: "Grapes", quantity: 0 },
{ id: "Apricots", quantity: 0 }
]
};
handleChange = (id, val) => {
this.setState(prevState => ({
items: prevState.items.map(item => {
const nextVal = item.quantity + val;
return id === item.id
? { id, quantity: nextVal > 0 ? nextVal : 0 }
: { ...item };
})
}));
};
render = () => (
<div className="container">
<h1>Updating Values Inside Array</h1>
{this.state.items.map(props => (
<div key={props.id} className="container">
<div>
{props.id} ({props.quantity})
</div>
<Button
{...props}
className="uk-button uk-button-primary"
handleChange={this.handleChange}
value={1}
>
+
</Button>
<Button
{...props}
disabled={props.quantity === 0}
className="uk-button uk-button-danger"
handleChange={this.handleChange}
value={-1}
>
-
</Button>
</div>
))}
</div>
);
}
components/button.js
import React, { PureComponent } from "react";
import PropTypes from "prop-types";
export default class Button extends PureComponent {
static propTypes = {
children: PropTypes.string.isRequired,
className: PropTypes.string,
disabled: PropTypes.bool,
id: PropTypes.string.isRequired,
handleChange: PropTypes.func.isRequired,
value: PropTypes.number.isRequired
};
handleClick = () => {
this.props.handleChange(this.props.id, this.props.value);
};
render = () => (
<button
disabled={this.props.disabled || false}
className={this.props.className}
onClick={this.handleClick}
style={{ marginRight: 10 }}
>
{this.props.children}
</button>
);
}

I know an answer has been accepted, but it doesn't actually satisfy the requirements fully, i.e.
Each callback should take a single, integer value as a parameter which is the amount to increment the counter's existing value by.
The accepted answer takes the event object as a parameter which is not the specified requirement. The only way to strictly satisfy the expected requirement is to bind unique
"...onIncrement callbacks as props..."
for each counter. This approach has drawbacks and performance implications as discussed in this article
Working example at https://codesandbox.io/s/j2vxj620z9

Related

Child component onClick parameter doesn't work in parent component

I set button onClick as a parameter in the child component and drag and use onClick in the parent component.
The child component Room .
type Props = {
items?: [];
onClick?: any;
}
const Room = ({ onClick, items: [] }: Props) => {
return (
<div>
{items.length ? (
<>
{items.map((item: any, index: number) => {
return (
<>
<button key={index} onClick={() => { console.log('hi'); onClick }}>{item.name}</button>
</>
)
}
</>
)
</div>
)
}
This is the parent component.
const LoadingRoom = () => {
const handleWaitingRoomMove = (e: any) => {
e.preventDefault();
console.log('Hello Move')
}
return (
<>
<Room
items={[
{
name: "Waiting Room",
onClick: {handleWaitingRoomMove}
},
{
name: "Host Room",
},
]}
>
</Room>
</>
)
}
I want to call parent component's onClick handleWaitingRoomMove but it's not getting called.
However, console.log('hi') on the button is called normally whenever the button is clicked. Only button is not called. Do you know why?
onlick is a attribute to the child element. so move it outside of the items array
<Room
onClick={handleWaitingRoomMove}
items={[
{
name: "Waiting Room",
},
{
name: "Host Room",
},
]}
>
In the child, missing () for onClick
onClick={(ev) => { console.log('hi'); onClick(ev) }}
Demo
You are passing onClick in items, not direct props
<button key={index} onClick={item.onClick}>{item.name}</button>
so your component will be
type Props = {
items?: [];
}
const Room = ({ items: [] }: Props) => {
return (
<div>
{items.length ? (
<>
{items.map((item: any, index: number) => {
return (
<>
<button key={index} onClick={item.onClick}>{item.name}</button>
</>
)
}
</>
)
</div>
)
}
It would probably be more advantageous to have one handler that does the work, and use that to identify each room by type (using a data attribute to identify the rooms). That way you keep your data and your component logic separate from each other. If you need to add in other functions at a later stage you can.
const { useState } = React;
function Example({ data }) {
// Handle the room type by checking the value
// of the `type` attribute
function handleClick(e) {
const { type } = e.target.dataset;
switch (type) {
case 'waiting': console.log('Waiting room'); break;
case 'host': console.log('Host Room'); break;
case 'guest': console.log('Guest Room'); break;
default: console.log('Unknown room'); break;
}
}
// Keep the room data and the handler separate
return (
<div>
{data.map(obj => {
return (
<Room
key={obj.id}
data={obj}
handleClick={handleClick}
/>
);
})}
</div>
);
}
// Apply a data attribute to the element
// you're clicking on, and just call the handler
// `onClick`
function Room({ data, handleClick }) {
const { type, name } = data;
return (
<button
data-type={type}
onClick={handleClick}
>{name}
</button>
);
}
const data = [
{ id: 1, type: 'waiting', name: 'Waiting Room' },
{ id: 2, type: 'host', name: 'Host Room' },
{ id: 3, type: 'guest', name: 'Guest Room' }
];
ReactDOM.render(
<Example data={data} />,
document.getElementById('react')
);
button:not(:last-child) { margin-right: 0.25em; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>

How to build a react button that stores the selection in an array

I am trying to create a list of buttons with values that are stored in a state and user is only allowed to use 1 item (I dont want to use radio input because I want to have more control over styling it).
import React from "react";
import { useEffect, useState } from "react";
import "./styles.css";
const items = [
{ id: 1, text: "Easy and Fast" },
{ id: 2, text: "Easy and Cheap" },
{ id: 3, text: "Cheap and Fast" }
];
const App = () => {
const [task, setTask] = useState([]);
const clickTask = (item) => {
setTask([...task, item.id]);
console.log(task);
// how can I make sure only 1 item is added to task
// and remove the other items
// only one option is selectable all the time
};
const chosenTask = (item) => {
if (task.find((v) => v.id === item.id)) {
return true;
}
return false;
};
return (
<div className="App">
{items.map((item) => (
<li key={item.id}>
<label>
<button
type="button"
className={chosenTask(item) ? "chosen" : ""}
onClick={() => clickTask(item)}
onChange={() => clickTask(item)}
/>
<span>{item.text}</span>
</label>
</li>
))}
</div>
);
};
export default App;
https://codesandbox.io/s/react-fiddle-forked-cvhivt?file=/src/App.js
I am trying to only allow 1 item to be added to the state at all the time, but I dont know how to do this?
Example output is to have Easy and Fast in task state and is selected. If user click on Easy and Cheap, select that one and store in task state and remove Easy and Fast. Only 1 item can be in the task state.
import React from "react";
import { useEffect, useState } from "react";
import "./styles.css";
const items = [
{ id: 1, text: "Easy and Fast" },
{ id: 2, text: "Easy and Cheap" },
{ id: 3, text: "Cheap and Fast" }
];
const App = () => {
const [task, setTask] = useState();
const clickTask = (item) => {
setTask(item);
console.log(task);
// how can I make sure only 1 item is added to task
// and remove the other items
// only one option is selectable all the time
};
return (
<div className="App">
{items.map((item) => (
<li key={item.id}>
<label>
<button
type="button"
className={item.id === task?.id ? "chosen" : ""}
onClick={() => clickTask(item)}
onChange={() => clickTask(item)}
/>
<span>{item.text}</span>
</label>
</li>
))}
</div>
);
};
export default App;
Is this what you wanted to do?
Think of your array as a configuration structure. If you add in active props initialised to false, and then pass that into the component you can initialise state with it.
For each task (button) you pass down the id, and active state, along with the text and the handler, and then let the handler in the parent extract the id from the clicked button, and update your state: as you map over the previous state set each task's active prop to true/false depending on whether its id matches the clicked button's id.
For each button you can style it based on whether the active prop is true or false.
If you then need to find the active task use find to locate it in the state tasks array.
const { useState } = React;
function Tasks({ config }) {
const [ tasks, setTasks ] = useState(config);
function handleClick(e) {
const { id } = e.target.dataset;
setTasks(prev => {
// task.id === +id will return either true or false
return prev.map(task => {
return { ...task, active: task.id === +id };
});
});
}
// Find the active task, and return its text
function findSelectedItem() {
const found = tasks.find(task => task.active)
if (found) return found.text;
return 'No active task';
}
return (
<section>
{tasks.map(task => {
return (
<Task
key={task.id}
taskid={task.id}
active={task.active}
text={task.text}
handleClick={handleClick}
/>
);
})};
<p>Selected task is: {findSelectedItem()}</p>
</section>
);
}
function Task(props) {
const {
text,
taskid,
active,
handleClick
} = props;
// Create a style string using a joined array
// to be used by the button
const buttonStyle = [
'taskButton',
active && 'active'
].join(' ');
return (
<button
data-id={taskid}
className={buttonStyle}
type="button"
onClick={handleClick}
>{text}
</button>
);
}
const taskConfig = [
{ id: 1, text: 'Easy and Fast', active: false },
{ id: 2, text: 'Easy and Cheap', active: false },
{ id: 3, text: 'Cheap and Fast', active: false }
];
ReactDOM.render(
<Tasks config={taskConfig} />,
document.getElementById('react')
);
.taskButton { background-color: palegreen; padding: 0.25em 0.4em; }
.taskButton:not(:first-child) { margin-left: 0.25em; }
.taskButton:hover { background-color: lightgreen; cursor: pointer; }
.taskButton.active { background-color: skyblue; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>

How to avoid rerendering all list items

On every click react rerender every item. How to avoid it? I want react to only render items that have changed. I tried using react memo and usecallback but it didn't help. I can't understand what is the reason. What are the ways to solve this problem? Thank you.
App.ts
import React, { useState } from "react";
import "./styles.css";
import ListItem from "./ListItem";
const items = [
{ id: 1, text: "items1" },
{ id: 2, text: "items2" },
{ id: 3, text: "items3" },
{ id: 4, text: "items4" },
{ id: 5, text: "items5" },
{ id: 6, text: "items6" },
{ id: 7, text: "items7" }
];
export default function App() {
const [activeIndex, setActiveIndex] = useState(1);
const onClick = (newActiveIndex: number) => {
setActiveIndex(newActiveIndex);
};
return (
<div className="App">
{items.map(({ id, text }) => (
<ListItem
key={id}
id={id}
text={text}
activeId={activeIndex}
onClick={onClick}
/>
))}
</div>
);
}
Item.ts
import React from "react";
interface ListItemProps {
id: number;
activeId: number;
text: string;
onClick: (newActiveIndex: number) => void;
}
const ListItem: React.FC<ListItemProps> = ({ id, activeId, text, onClick }) => {
const isActive = activeId === id;
const style = {
marginRight: "42px",
marginTop: "24px",
color: isActive ? "green" : "red"
};
console.log(`update id: ${id}`);
return (
<div>
<span style={style}>id: {id}</span>
<span style={style}>is active: {isActive ? "active" : "inactive"}</span>
<span style={style}>text: {text}</span>
<button onClick={() => onClick(id)}>setActive</button>
</div>
);
};
export default ListItem;
activeIndex changes with each click, so all of the components have new prop values and will need to re-render.
Instead of passing the activeIndex to every component every time:
const ListItem: React.FC<ListItemProps> = ({ id, activeId, text, onClick }) => {
const isActive = activeId === id;
Pass an isActive to each component:
const ListItem: React.FC<ListItemProps> = ({ id, isActive, text, onClick }) => {
Effectively moving the calculation of the bool from the component to the consuming code:
<ListItem
key={id}
id={id}
text={text}
isActive={activeIndex === id}
onClick={onClick}
/>
Then memoization (coupled with useCallback for the onClick handler) should work because most components (the ones which aren't changing their "active" state) won't receive new props and won't need to re-render.
Even with memoization it won't work, because your onClick function changes your components state, and your children components depend on activeIndex value, so every time you click you change the components state, and when state changes, the component re-renders itself and it's children, if your children didn't depend on your state, they wouldn't re-render (if you use memoization).
Plus to David's answer i would return ListItem in React.memo React.memo(ListItem)

Adding clicked items to new array in React

I am making API calls and rendering different components within an object. One of those is illustrated below:
class Bases extends Component {
constructor() {
super();
this.state = {
'basesObject': {}
}
}
componentDidMount() {
this.getBases();
}
getBases() {
fetch('http://localhost:4000/cupcakes/bases')
.then(results => results.json())
.then(results => this.setState({'basesObject': results}))
}
render() {
let {basesObject} = this.state;
let {bases} = basesObject;
console.log(bases);
//FALSY values: undefined, null, NaN, 0, false, ""
return (
<div>
{bases && bases.map(item =>
<button key={item.key} className="boxes">
{/* <p>{item.key}</p> */}
<p>{item.name}</p>
<p>${item.price}.00</p>
{/* <p>{item.ingredients}</p> */}
</button>
)}
</div>
)
}
}
The above renders a set of buttons. All my components look basically the same.
I render my components here:
class App extends Component {
state = {
ordersArray: []
}
render() {
return (
<div>
<h1>Bases</h1>
<Bases />
<h1>Frostings</h1>
<Frostings />
<h1>Toppings</h1>
<Toppings />
</div>
);
}
}
I need to figure out the simplest way to, when a button is clicked by the user, add the key of each clicked element to a new array and I am not sure where to start. The user must select one of each, but is allowed to select as many toppings as they want.
Try this
We can use the same component for all categories. All the data is handled by the parent (stateless component).
function Buttons({ list, handleClick }) {
return (
<div>
{list.map(({ key, name, price, isSelected }) => (
<button
className={isSelected ? "active" : ""}
key={key}
onClick={() => handleClick(key)}
>
<span>{name}</span>
<span>${price}</span>
</button>
))}
</div>
);
}
Fetch data in App component, pass the data and handleClick method into Buttons.
class App extends Component {
state = {
basesArray: [],
toppingsArray: []
};
componentDidMount() {
// Get bases and toppings list, and add isSelected attribute with default value false
this.setState({
basesArray: [
{ key: "bases1", name: "bases1", price: 1, isSelected: false },
{ key: "bases2", name: "bases2", price: 2, isSelected: false },
{ key: "bases3", name: "bases3", price: 3, isSelected: false }
],
toppingsArray: [
{ key: "topping1", name: "topping1", price: 1, isSelected: false },
{ key: "topping2", name: "topping2", price: 2, isSelected: false },
{ key: "topping3", name: "topping3", price: 3, isSelected: false }
]
});
}
// for single selected category
handleSingleSelected = type => key => {
this.setState(state => ({
[type]: state[type].map(item => ({
...item,
isSelected: item.key === key
}))
}));
};
// for multiple selected category
handleMultiSelected = type => key => {
this.setState(state => ({
[type]: state[type].map(item => {
if (item.key === key) {
return {
...item,
isSelected: !item.isSelected
};
}
return item;
})
}));
};
// get final selected item
handleSubmit = () => {
const { basesArray, toppingsArray } = this.state;
const selectedBases = basesArray.filter(({ isSelected }) => isSelected);
const selectedToppings = toppingsArray.filter(({ isSelected }) => isSelected);
// submit the result here
}
render() {
const { basesArray, toppingsArray } = this.state;
return (
<div>
<h1>Bases</h1>
<Buttons
list={basesArray}
handleClick={this.handleSingleSelected("basesArray")}
/>
<h1>Toppings</h1>
<Buttons
list={toppingsArray}
handleClick={this.handleMultiSelected("toppingsArray")}
/>
</div>
);
}
}
export default App;
CSS
button {
margin: 5px;
}
button.active {
background: lightblue;
}
I think the following example would be a good start for your case.
Define a handleClick function where you can set state with setState as the following:
handleClick(item) {
this.setState(prevState => {
return {
...prevState,
clickedItems: [...prevState.clickedItems, item.key]
};
});
}
Create an array called clickedItems in constructor for state and bind handleClick:
constructor() {
super();
this.state = {
basesObject: {},
clickedItems: [],
}
this.handleClick = this.handleClick.bind(this);
}
You need to add a onClick={() => handleClick(item)} handler for onClick:
<button key={item.key} className="boxes" onClick={() => handleClick(item)}>
{/* <p>{item.key}</p> */}
<p>{item.name}</p>
<p>${item.price}.00</p>
{/* <p>{item.ingredients}</p> */}
</button>
I hope that helps!

React state keep old state

I have some issue in React that seems to keep last or old state.
I have a parent component called MyLists.js that contain a loop function where I rendered child component called Item.js
{
this.state.listProducts.map(d =>
<Item data={d} />
)}
And in my Item.js component I set state in constructor :
this.state = { showFullDescription: false }
The variable "showFullDescription" allows me to see the entire description of a product. Now I have for example 2 products and all states "showFullDescription" are set to false so :
Product 1 => (showFullDescription = false)
Product 2 => (showFullDescription = false)
Next, I show full description for Product 2 by clicking a button and I set state to true so Product 2 => (showFullDescription = true)
The problem is when I add another product, let's call it "Product 3", the full description of "Product 3" is directly shown and for "Product 2" it is hidden. It seems that last state is reflected on "Product 3".
I am really sorry for my english, it's not my native language
Here is full source code :
MyLists.js
import React, { Component } from 'react';
import ProductService from '../../../../services/ProductService';
import Item from './Item';
class MyLists extends Component {
constructor(props) {
super(props);
this.state = {
products: []
}
this.productService = new ProductService();
this.productService.getAllProducts().then((res) => {
this.setState({
products: res
})
});
}
addProduct(data){
this.productService.addProduct(data).then((res) => {
var arr = this.state.products;
arr.push(res);
this.setState({
products: arr
})
})
}
render() {
return (
<div>
{
this.state.products.map(d =>
<Item data={d} />
)}
</div>
)
}
}
export default MyLists;
Item.js
import React, { Component } from 'react';
import Truncate from 'react-truncate';
class Item extends Component {
constructor(props) {
super(props);
this.state = {
showFullDescription: false
}
}
render() {
return (
<div>
<h2>{this.props.data.title}</h2>
{
!this.state.showFullDescription &&
<Truncate lines={10} ellipsis={<a className="btn btn-primary read-more-btn" onClick={() => this.setState({showFullDescription: true})}>Show More</a>}>
{this.props.data.description}
</Truncate>
)}
{
this.state.showFullDescription &&
<span>
{this.props.data.description}
</span>
}
</div>
)
}
}
export default Item;
You have some syntax problems and missing && for !this.state.showFullDescription.
I've slightly changed the component and use ternary operator to render conditionally. It is a little bit ugly right now, the logic can be written outside of the render. Also, I suggest you to use a linter if you are not using.
MyLists.js
class MyLists extends React.Component {
state = {
products: [
{ id: 1, description: "Foooooooooooooooooooooooooooooooooooooooooo", title: "first" },
{ id: 2, description: "Baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaarrrrrrrrrrr", title: "second" },
{ id: 3, description: "Baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaz", title: "third" },
]
}
render() {
return (
<div>
{
this.state.products.map(d =>
<Item data={d} />
)}
</div>
)
}
}
Item.js
class Item extends React.Component {
state = {
showFullDescription: false,
}
render() {
return (
<div>
<h2>{this.props.data.title}</h2>
{ !this.state.showFullDescription
?
<Truncate lines={3} ellipsis={<span>... <button onClick={() => this.setState({showFullDescription: true})}>Read More</button></span>}>
{this.props.data.description}
</Truncate>
:
<span>
{this.props.data.description}
</span>
}
</div>
)
}
}
Here is working sandbox: https://codesandbox.io/s/x24r7k3r9p
You should try mapping in the second component as:
class Item extends React.Component {
state = {
showFullDescription: false,
}
render() {
return (
<div>
{this.props..data.map(data=>
<h2>{this.props.data.title}</h2>
{ !this.state.showFullDescription
?
<Truncate lines={3} ellipsis={<span>... <button onClick={() =>
this.setState({showFullDescription: true})}>Read More</button>
</span>}>
{this.props.data.description}
</Truncate>
:
<span>
{this.props.data.description}
</span>)}
}
</div>
)
}
}
You should have a 'key' property (with unique value) in 'Item' - No warnings about it in console?

Categories

Resources