I try to write to make my own custom component but it doesn't work as I wish. The interaction seems ok but the it's only render the last item of the arrays.
export default class MyComponent extends Component {
constructor() {
super()
this.state = {
openItems: false,
selectedItem: 'Please select'
}
}
render() {
const { items, className } = this.props
const { openItems, selectedItem } = this.state
return (
<div className={classnames('myComponent', className)}>
<div tabIndex="1"
onBlur={() => this.setState({ openItems: false })}
onFocus={() => this.setState({ openItems: true })}>
{selectedItem}
<div className={classnames({'show': openItems === true, 'hide': openItems === false})}>
{items.map((obj, i) => {
return(
<li onClick={() => this.setState({ selectedItem: obj.name, openItems: false })}
key={i}>
{obj.name}
</li>
)
})}
</div>
</div>
</div>
)
}
}
and somewhere I used the component like this
<MyComponent items={[{
name: 'abc',
name: 'def',
name: 123
}]} />
I have no clue what the mistake is.
Your component expects an array of object with the key name. When you've initialized your component, you've only passed in a single object with the key name duplicated three times:
<MyComponent items={[{
name: 'abc',
name: 'def', // <-- this overrides the previous 'abc'
name: 123 // <-- this overrides the previous 'def'
}]} />
What you want is this:
<MyComponent items={[
{ name: 'abc' },
{ name: 'def' },
{ name: 123 },
]} />
You seem to be passing the same object name thrice, this will override the previous values and the latest value will take the final form.
Consider passing different values with different names.
<MyComponent items={[{ name: 'abc',
name: 'def',
name: 123
}]} />
pass the props into the constructor method as I am looking you have not passed the props so let's try to add props into the constructor as given below:-
constructor(props) {
super(props)
this.state = {
openItems: false,
selectedItem: 'Please select'
}
}
Related
I have an component that renders different types of fields called Item. Item may render a select box with a list of Users or a list of Inventory. I have two containers: one for Users and another for Inventory. I originally thought to nest my containers but that appears to freeze my react app. Inventories and Users containers are identical except that one container holds inventory items and the other holds users.
Here is the Users container:
import React, { Component } from 'react';
class UsersContainer extends Component{
constructor(props){
super(props);
this.state = {
users: []
}
}
componentDidMount(){
//put api call here
this.setState({users: [{id: 1, name: "Test Name", email: "test#yahoo.com"}, {id: 2, name: "John Doe", email: "johndoe#gmail.com"}, {id: 3, name: "Jane Doe", email: "janedoe#yahoo.com"}]})
}
render(){
return(
<div className="users-container">
{React.Children.map(this.props.children, child => (
React.cloneElement(child, {...this.props, users: this.state.users })
))}
</div>
)
}
}
export default UsersContainer;
I originally tried to nest the containers but this causes React to freeze:
<UsersContainer>
<InventoriesContainer>
{this.props.items.map(i => (
<Item name={i.name} />
))}
</InventoriesContainer>
</UsersContainer>
Item looks something like this:
function elementUsesInvetory(inventories){
//returns selectbox with list of inventory
}
function elementUsesUsers(users){
//returns selectbox with list of users
}
function Item(props){
render(){
return(
<>
{elementUsesUsers(props.inventories)}
{elementUsesInventory(props.users)}
</>
);
}
}
How can I provide the data from UsersContainer and InventoriesContainer to the Item component?
Merging them into one component would avoid a lot of confusion. If you still want to nest them, you might want to pass the props by prop-drilling or by using the context API. React.cloneElement isn't preferred for nested child components. More on that here
You can pass down the data with the help of React's context API. The UsersContainer component holds the Provider and passes users down to Inventories
The Inventories will then pass on the users and inventories as props to the Items component. I'm not sure if you need separate functions for the select boxes but I've added them in the demo anyway.
const MyContext = React.createContext();
class UsersContainer extends React.Component {
constructor(props) {
super(props);
this.state = {
users: []
};
}
componentDidMount() {
//put api call here
this.setState({
users: [
{ id: 1, name: "Test Name", email: "test#yahoo.com" },
{ id: 2, name: "John Doe", email: "johndoe#gmail.com" },
{ id: 3, name: "Jane Doe", email: "janedoe#yahoo.com" }
]
});
}
render() {
return (
<div className="users-container">
<MyContext.Provider value={this.state.users}>
{this.props.children}
</MyContext.Provider>
</div>
);
}
}
class Inventories extends React.Component {
static contextType = MyContext;
constructor(props) {
super(props);
this.state = {
inventories: []
};
}
componentDidMount() {
//put api call here
this.setState({
inventories: [
{ id: 1, name: "Test Name", email: "test#yahoo.com" },
{ id: 2, name: "John Doe", email: "johndoe#gmail.com" },
{ id: 3, name: "Jane Doe", email: "janedoe#yahoo.com" }
]
});
}
render() {
return (
<div className="inventory-container">
{React.Children.map(this.props.children, (child) => {
return React.cloneElement(child, {
...this.props,
users: this.context,
inventories: this.state.inventories
});
})}
</div>
);
}
}
function Items(props) {
function usersSelect(items) {
return (
<select>
{items.map((item) => (
<option key={"user"+item.id} value="{item.id}">
{item.name}
</option>
))}
</select>
);
}
function inventoriesSelect(items) {
return (
<select>
{items.map((item) => (
<option key={item.id} value="{item.id}">
{item.name}
</option>
))}
</select>
);
}
return (
<div>
<h2>users</h2>
{usersSelect(props.users)}
<h2>inventories</h2>
{inventoriesSelect(props.inventories)}
</div>
);
}
function App() {
return (
<div>
<UsersContainer>
<Inventories>
<Items />
</Inventories>
</UsersContainer>
</div>
);
}
ReactDOM.render(<App/>, document.getElementById("root"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I good approach would be to put the state in common between those components in a level up in the tree component.
So what are you trying to do:
<UsersContainer>
<InventoriesContainer>
{this.props.items.map(i => (
<Item name={i.name} />
))}
</InventoriesContainer>
</UsersContainer>
Would be:
RealFatherComponent extends Component {
// state that Item will need will be set here
render() {
return (
< UsersContainer **propsShared** >
<Item **propsShared** />
</UsersContainer>
< InventoriesContainer **propsShared** >
<Item **propsShared** /> );
</InventoriesContainer>
}
}
I have two classes. One holds the array, the other holds the array props. These are my classes:
//PARENT CLASS:
constructor() {
super()
this.state = {
items: []
}
this.addItem = this.addItem.bind(this)
}
componentDidMount(){
this.setState({
items: [{
name: 'Sebastian',
num: '001'
},{
name: 'Josh',
num: '002'
}]
})
}
addItem() {
??????
}
render() {
return(
<div>
<MethodA items={this.state.items} addItem={this.addItem}/>
</div>
)
}
//CHILD CLASS:
function MethodA(props) {
return(
<div>
{props.items.map((item, i) =>{
return(<div key={i}>
<span>{item.name}</span>
<span>{item.num}</span>
</div>)
})}
<button onClick={() => { props.addItem() }}>ADD ITEM</button>
</div>
)
}
Current result is like this:
<div>
<span>Sebastian</span>
<span>001</span>
</div>
<div>
<span>Sebastian</span>
<span>002</span>
</div>
Then after the "ADD ITEM" button was hit, this will be the new result:
<div>
<span>Sebastian</span>
<span>001</span>
</div>
<div>
<span>Sebastian</span>
<span>002</span>
</div>
<div>
<span>New Name</span>
<span>New Num</span>
</div>
I'm not sure whether what and how to use between push() or concat() or both. Any ideas?
Firstly, there's no need to set the initial state in componentDidMount, you can do it directly in constructor.
constructor(props) {
super(props);
this.state = {
items: [
{
name: "Sebastian",
num: "001"
},
{
name: "Josh",
num: "002"
}
]
};
this.addItem = this.addItem.bind(this);
}
To add an item you can use functional form of setState and you'll need to pass that item into callback from the child component.
addItem(item) {
this.setState(state => ({
items: [...state.items, item]
}));
}
// Child class
function MethodA(props) {
return(
<div>
{props.items.map((item, i) =>{
return(<div key={i}>
<span>{item.name}</span>
<span>{item.num}</span>
</div>)
})}
<button onClick={() => props.addItem(item)}>ADD ITEM</button> // Pass item to the parent's method
</div>
)
}
Here's the deal. The difference between push() and concat() is in immutability.
If you use push on an array, it will mutate the original array and add a new value to that array (wrong).
If you use concat, it will create a new array for you, leaving the old array untouched (correct).
So you might want to do something along these lines:
addItem(item)
this.setState(state => {
const items = state.items.concat(item);
return {
items,
};
});
}
I have a simple to do app that is working fine, except for the ability to delete items from the list. I have already added the button to each of the list items. I know I want to use the .filter() method to pass the state a new array that doesn't have the deleted to-do but I'm not sure how to do something like this.
Here is the App's main component:
class App extends Component {
constructor(props){
super(props);
this.state = {
todos: [
{ description: 'Walk the cat', isCompleted: true },
{ description: 'Throw the dishes away', isCompleted: false },
{ description: 'Buy new dishes', isCompleted: false }
],
newTodoDescription: ''
};
}
deleteTodo(e) {
this.setState({ })
}
handleChange(e) {
this.setState({ newTodoDescription: e.target.value })
}
handleSubmit(e) {
e.preventDefault();
if (!this.state.newTodoDescription) { return }
const newTodo = { description: this.state.newTodoDescription,
isCompleted: false };
this.setState({ todos: [...this.state.todos, newTodo],
newTodoDescription: '' });
}
toggleComplete(index) {
const todos = this.state.todos.slice();
const todo = todos[index];
todo.isCompleted = todo.isCompleted ? false : true;
this.setState({ todos: todos });
}
render() {
return (
<div className="App">
<ul>
{ this.state.todos.map( (todo, index) =>
<ToDo key={ index } description={ todo.description }
isCompleted={ todo.isCompleted } toggleComplete={ () =>
this.toggleComplete(index) } />
)}
</ul>
<form onSubmit={ (e) => this.handleSubmit(e) }>
<input type="text" value={ this.state.newTodoDescription }
onChange={ (e) => this.handleChange(e) } />
<input type="submit" />
</form>
</div>
);
}
}
And then here is the To-Do's component:
class ToDo extends Component {
render() {
return (
<li>
<input type="checkbox" checked={ this.props.isCompleted }
onChange={ this.props.toggleComplete } />
<button>Destroy!</button>
<span>{ this.props.description }</span>
</li>
);
}
}
Event handlers to the rescue:
You can send onDelete prop to each ToDo:
const Todo = ({ description, id, isCompleted, toggleComplete, onDelete }) =>
<li>
<input
type="checkbox"
checked={isCompleted}
onChange={toggleComplete}
/>
<button onClick={() => onDelete(id)}>Destroy!</button>
<span>{description}</span>
</li>
And from App:
<ToDo
// other props here
onDelete={this.deleteTodo}
/>
As pointed by #Dakota, using index as key while mapping through a list is not a good pattern.
Maybe just change your initialState and set an id to each one of them:
this.state = {
todos: [
{ id: 1, description: 'Walk the cat', isCompleted: true },
{ id: 2, description: 'Throw the dishes away', isCompleted: false },
{ id: 3, description: 'Buy new dishes', isCompleted: false }
],
newTodoDescription: '',
}
This also makes life easier to delete an item from the array:
deleteTodo(id) {
this.setState((prevState) => ({
items: prevState.items.filter(item => item.id !== id),
}))
}
Before you get any further you should never use a list index as the key for your React Elements. Give your ToDo an id and use that as the key. Sometimes you can get away with this but when you are deleting things it will almost always cause issues.
https://medium.com/#robinpokorny/index-as-a-key-is-an-anti-pattern-e0349aece318
If you don't want to read the article, just know this
Let me explain, a key is the only thing React uses to identify DOM
elements. What happens if you push an item to the list or remove
something in the middle? If the key is same as before React assumes
that the DOM element represents the same component as before. But that
is no longer true.
On another note, add an onClick to your button and pass the function you want it to run as a prop from App.
<button onClick={() => this.props.handleClick(this.props.id)} />
and App.js
...
constructor(props) {
...
this.handleClick = this.handleClick.bind(this);
}
handleClick(id) {
// Do stuff
}
<ToDo
...
handleClick={this.handleClick}
/>
I'm currently writing my first react application and my ESLINT is telling me that I shouldn't be using .bind() on JSX props. I understand that this is because bind is creating new functions and therefore negatively affecting performance. However i am not sure how to refactor this to eliminate this error.
How can i pass the element I have clicked to the function without using a bind?
ForecastPage.jsx:
import React from 'react'
import api from '../shared/api'
import ForecastBox from './ForecastBox'
import DropdownSelector from './DropdownSelector'
const regions = [
{
name: 'Santa Cruz',
id: '2958',
spots:
[
{ name: 'Steamer Lane', id: '4188' },
{ name: 'Four Mile', id: '5023' },
{ name: 'Waddell Creek', id: '5021' },
{ name: 'Mitchell\'s Cove', id: '5028' },
{ name: '26th Ave', id: '5030' },
],
},
{
name: 'North Orange Country',
id: '2143',
spots:
[
{ name: 'Newport', id: '1241' },
{ name: 'HB', id: '3421' },
],
},
]
class ForecastPage extends React.Component {
constructor(props) {
super(props)
this.state = {
selectedRegion: null,
selectedSpot: null,
forecast: null,
}
this.regionSpotList = regions
this.updateSpot = this.updateSpot.bind(this)
this.updateRegion = this.updateRegion.bind(this)
}
updateRegion(region) {
this.setState({
selectedRegion: region,
forecast: null,
})
api.fetchSpot(region.id)
.then((forecast) => {
this.setState({
forecast,
})
})
}
updateSpot(spot) {
this.setState({
selectedSpot: spot,
forecast: null,
})
api.fetchSpot(spot.id)
.then((forecast) => {
this.setState({
forecast,
})
})
}
render() {
return (
<div>
<div className="container-fluid row region-spot-select">
<DropdownSelector
options={this.regionSpotList}
onSelect={this.updateRegion}
title={this.state.selectedRegion == null ? 'Select Your Region' : this.state.selectedRegion.name}
keyName={'region-selector'}
id={'region-selector-dropdown'}
/>
{this.state.selectedRegion != null &&
<DropdownSelector
options={this.state.selectedRegion.spots}
onSelect={this.updateSpot}
title={this.state.selectedSpot == null ||
!this.state.selectedRegion.spots.includes(this.state.selectedSpot) ?
'Select A Spot' :
this.state.selectedSpot.name}
keyName={'spot-selector'}
id={'spot-selector-dropdown'}
/>
}
</div>
<div>
{!this.state.forecast ?
<div>
Select A Region
</div>
: <ForecastBox forecast={this.state.forecast} /> }
</div>
</div>
)
}
}
export default ForecastPage
DropdownSelector.jsx
// #flow
import React from 'react'
import PropTypes from 'prop-types'
import { DropdownButton, MenuItem } from 'react-bootstrap'
type Props = {
options: Object,
onSelect: Function,
title: string,
keyName: string,
id: string,
}
const DropdownSelector = ({ title, options, keyName, id, onSelect }: Props) =>
<div className="content">
<div className="btn-group">
<DropdownButton
bsStyle={'primary'}
title={title}
key={keyName}
id={id}
>
{options.map(element =>
<MenuItem
key={element.name}
eventKey={element.name}
// eslint-disable-next-line
onClick={onSelect.bind(null, element)}
>
{element.name}
</MenuItem>,
)
}
</DropdownButton>
</div>
</div>
DropdownSelector.defaultProps = {
id: null,
}
DropdownSelector.propTypes = {
options: PropTypes.instanceOf(Object).isRequired,
title: PropTypes.string.isRequired,
onSelect: PropTypes.func.isRequired,
keyName: PropTypes.string.isRequired,
id: PropTypes.string,
}
export default DropdownSelector
Try Alex's answer, but just onSelect, without 'this'.
You could also use an arrow function which would accomplish the same thing, so something like
onClick={(event) => this.props.onSelect(null, element)}
However, it has the same potential negative performance problem you mentioned. The React docs are very good in this area and enumerate your options and their pro's and cons: https://facebook.github.io/react/docs/handling-events.html
Updated to this.props.onSelect, forgot that you were passing that in as a prop as opposed to defining it on the component itself. And if you're not using the event object, perhaps just use
onClick={() => this.props.onSelect(null, element)}
I'm still getting to grips with react but I can't see why this isn't working, it should be passing the props from tabs into <Tab /> and outputting the button each time.
If I put no text next to {this.props.content} it doesn't display anything, if I put testText next to {this.props.content} it will output the button 5 times but only display testText not the name field it should be displaying via the content={item.name} prop
class TopCategories extends React.Component {
render() {
const Tab = () => (
<TestBtn key={this.props.key} >
testText {this.props.content}
</TestBtn>
)
const items = [
{ id: 1, name: 'tab-1', text: 'text' },
{ id: 2, name: 'tab-2', text: 'text' },
{ id: 3, name: 'tab-3', text: 'text' },
{ id: 4, name: 'tab-4', text: 'text' },
{ id: 5, name: 'tab-5', text: 'text' },
]
const tabs = items.map(item =>
<Tab key={item.id} content={item.name} />,
)
return (
<Container>
<Wrapper>
{tabs}
</Wrapper>
</Container>
)
}
}
export default TopCategories
You need to pass props to the stateless function and since it's a stateless component, this is not available. It should be something like:
const Tab = (props) => {
return (
<TestBtn key={props.key} >
testText {props.content}
</TestBtn>
);
}