I have a parent class-based component A and a child functional component B. Inside B I map over a list of names and render them as li elements, which onClick call the onLanguageUpdate handler declared in the parent component, and what this handler does is update the state to reflect the selected name.
Question then:
I need to call a second event handler in the same onClick, this time to change the color of the name the user has clicked on. I added a new property to the state, color, to represent a className that I can then toggle with the handleStyleColorChange handler. But how do I get the li elements in the child component to update their className (or style) based on the result of this handler? If I was doing all of this inside component A's render method, I could do style={language === this.state.selectedLanguage ? {color: 'red'} : null} on the li and call it a day.
// Component A
import React, { Component } from 'react';
import B from './B';
class A extends Component {
constructor(props) {
super(props);
this.state = {
selectedLanguage: 'All',
color: 'lang-black-text'
};
}
handleUpdateLanguage = (language) => {
return this.setState({ selectedLanguage: language });
}
handleStyleColorChange = (language) => {
if (language === this.state.selectedLanguage) {
return this.setState({ color: 'lang-red-text' });
} else {
return this.setState({ color: 'lang-black-text' });
}
}
handleClick = (language) => {
this.handleUpdateLanguage(language);
this.handleStyleColorChange(language);
}
render() {
return (
<LanguageList onLanguageUpdate={this.handleClick} />
);
}
}
export default A;
// Component B
import React from 'react';
const B = (props) => {
const languages = ['English', 'Spanish', 'Japanese', 'Italian'];
const languageListFormatted = languages.map(language => {
return (
<li
key={language}
onClick={() => props.onLanguageUpdate(language)}>{language}
</li>
);
});
return (
<ul className="languages">{languageListFormatted}</ul>
);
}
export default B;
You can't manage the color from the parent comp, it needs to be done from the child comp. Then, send the selectedLanguage to the child and you are good.
class A extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedLanguage: 'All',
color: 'lang-black-text'
};
}
handleUpdateLanguage = (language) => {
return this.setState({ selectedLanguage: language });
}
handleStyleColorChange = (language) => {
if (language === this.state.selectedLanguage) {
return this.setState({ color: 'lang-red-text' });
} else {
return this.setState({ color: 'lang-black-text' });
}
}
handleClick = (language) => {
this.handleUpdateLanguage(language);
this.handleStyleColorChange(language);
}
render() {
return (
<B
onLanguageUpdate={this.handleClick}
selectedLanguage={this.state.selectedLanguage}
/>
);
}
}
const B = (props) => {
const languages = ['English', 'Spanish', 'Japanese', 'Italian'];
const languageListFormatted = languages.map(language => {
return (
<li
key={language}
style={props.selectedLanguage === language ? {background: 'yellow'} : {}}
onClick={() => props.onLanguageUpdate(language)}>{language}
</li>
);
});
return (
<ul className="languages">{languageListFormatted}</ul>
);
}
ReactDOM.render(
<A />,
document.getElementById('app')
);
<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="app"></div>
Related
I have a tab navigation at the top of my page, and I want to add an 'active' class to the tab when it's clicked and make sure it's not on any of the other tabs.
So far what I have adds the 'active' class to the first tab, but doesn't update if you click on any of the other tabs. So the first tab is always the active tab regardless of what you click on.
import React from 'react'
import { string } from 'prop-types'
class TabNav extends React.Component {
constructor(props) {
super(props)
this.state = {
currentTab: ''
}
this.handleClick = this.handleClick.bind(this)
this.createTabItems = this.createTabItems.bind(this)
}
shouldComponentUpdate (nextProps, nextState) {
return nextProps.navItems !== this.props.navItems
}
handleClick (currentTab) {
this.setState({
currentTab: currentTab
})
this.createTabItems()
}
createTabItems () {
const { navItems = false } = this.props
if (!navItems) return false
const splitItems = navItems.split(',')
if (!splitItems.length) return false
return splitItems.map((item, currentTab) => {
const items = item.split('_')
if (items.length !== 3) return null
const itemLink = items[1]
return (
<li key={currentTab} className={this.state.currentTab == currentTab ? 'side-nav-tab active' : 'side-nav-tab'} onClick={this.handleClick.bind(this, currentTab)}>
<a href={itemLink}>
<p>{ items[0] }</p>
</a>
</li>
)
})
}
render () {
const tabItems = this.createTabItems()
if (!tabItems) return null
return (
<div>
<ul id='tabNavigation'>
{tabItems}
</ul>
</div>
)
}
}
TabNav.propTypes = {
navItems: string.isRequired
}
export default TabNav
I have also tried calling this.createTabItems asynchronously in setState to try and force an update but that didn't work:
handleClick (currentTab) {
this.setState({
currentTab: currentTab
}, () => this.createTabItems)
}
I think your shouldComponentUpdate is causing the issue. Can you try removing it? Also, you don't need to call this.createTabItems in your handleClick()
I feel like a mindset shift is required here to think more declaratively, you don't need to tell React when you want to create the tabs, it will determine this itself from the render method and the data that's passed to it.
I've removed your componentShouldUpdate function because React will already do the comparison of those props for you. I've also tracked the selected item by index because it simplifies the logic in my mind.
Try something like this:
import React from 'react';
import { string } from 'prop-types';
class TabNav extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedIndex: 0
};
}
handleClick = index => {
this.setState({
selectedIndex: index
});
};
render() {
const { navItems = [] } = this.props;
return (
<div>
<ul id="tabNavigation">
{navItems.map((navItem, index) => {
if (navItem.length !== 3) return;
const [text, link] = navItem;
return (
<li
className={
this.state.selectedIndex === index
? 'side-nav-tab active'
: 'side-nav-tab'
}
onClick={this.handleClick.bind(null, index)}
>
<a href={link}>
<p>{text}</p>
</a>
</li>
);
})}
}
</ul>
</div>
);
}
}
TabNav.propTypes = {
navItems: string.isRequired
};
export default TabNav;
There are a couple other changes in there, like destructuring from the array rather than using indexes so it's clearer what the properties you're pulling out of a navItem array are.
I also used a fat arrow function for the handleClick function because it means you don't have to bind to this in your constructor.
You got two problems, first the way you are using this in your function and secondly your shouldUpdateComponent, just like the other mentioned before me. If you want to reference your class using this you need to use arrow functions. remember that the ES6 arrow function uses lexical scoping which means that this references the code that contains the function. I made the changes in the code below with a working example.
class TabNav extends React.Component {
constructor(props) {
super(props)
this.state = {
currentTab: ''
}
this.handleClick = this.handleClick.bind(this)
this.createTabItems = this.createTabItems.bind(this)
}
handleClick (currentTab) {
this.setState({
currentTab: currentTab
})
}
createTabItems = () => {
const { navItems = false } = this.props
if (!navItems) return false
const splitItems = navItems.split(',')
if (!splitItems.length) return false
return splitItems.map((item, currentTab) => {
const items = item.split('_')
if (items.length !== 3) return null
const itemLink = items[1]
return (
<li key={currentTab} className={this.state.currentTab == currentTab ? 'side-nav-tab active' : 'side-nav-tab'} onClick={this.handleClick.bind(null, currentTab)}>
<a href={itemLink}>
<p>{ items[0] }</p>
</a>
</li>
)
})
}
render () {
const tabItems = this.createTabItems()
if (!tabItems) return null
return (
<div>
<ul id='tabNavigation'>
{tabItems}
</ul>
</div>
)
}
}
class Nav extends React.Component {
render() {
return(
<TabNav navItems={'Test1_#_Test1,Test2_#_Test2'} />)
}
}
ReactDOM.render(<Nav />, document.getElementById('root'))
.active {
font-size: 20px;
}
<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'm trying to update more than 5 components' status after a button is clicked. I'm now putting all the status and event handlers in App.js. For example,
// MyButton.js
export const MyButton = (props) => {
<button type='button' onClick={props.onClick}>Click Me!</button>
}
// Component1.js
export const Component1 = ({ name }) => {
return (
<h1>{name}</h1>
);
}
// Component2.js
export const Component2 = ({ name }) => {
return (
<h1>{name}</h1>
);
}
App.js
class App extends React.Component {
constructor(props) {
super(props);
this.state = { name1: '', name2: '' };
this.changeNames = this.changeNames.bind(this);
}
changeNames() {
this.changeComponent1Name();
this.changeComponent2Name();
}
changeComponent1Name() {
this.setState({ name1: 'name1' });
}
changeComponent2Name() {
this.setState({ name2: 'name2' });
}
render() {
return (
<MyButton onClick={this.changeName} />
<Component1 name={this.state.name1} />
<Component2 name={this.state.name2} />
);
}
}
ReactDOM.render(<App />, document.getElementById('app'));
The code becomes more complicated as more components I have to update when the button is clicked. I'd like to put the status and event handlers in each components so the code can be more readable. Any suggestions?
just loop over state:
changeNames() {
let newState = {...this.state}
for (var stateProp in newState) {
//add here your logic depending on prop name/for every prop
}
}
I have a PureComponent that renders another component and implements its onClick callback:
class ColorPicker extends React.PureComponent {
render() {
console.log('ColorPicker being rendered');
const fields = this.props.colors.map((color, idx) => {
const fieldProps = {
key: `${idx}`,
color,
/*onClick: () => { // PROBLEM HERE
this.props.colorPicked(color);
}*/
};
return <ColorField { ...fieldProps}/>;
});
return (
<div className="bla-picker">
<div>{`Refresh seed: ${this.props.seed}`}</div>
{fields}
< /div>
);
}
}
There is a small issue with this component: Whenever the ColorPicker is re-rendered, the nested ColorFields need to be re-rendered, too, because their onClick property changes each time. Using a lambda function will create a new instance of that function whenever the component is rendered.
I usually solve this by moving the implementation of onClick outside of the render method, like this: onClick: this.handleClick. However, I can't do this here, because the onClick handler needs to capture the color variable.
What's the best practice to solve this kind of problem?
Here's a jsfiddle to try it out; and as a snippet:
class ColorField extends React.PureComponent {
render() {
console.log('ColorField being rendered');
const divProps = {
className: 'bla-field',
style: {
backgroundColor: this.props.color
},
onClick: this.props.onClick
};
return <div { ...divProps}/>;
}
}
class ColorPicker extends React.PureComponent {
render() {
console.log('ColorPicker being rendered');
const fields = this.props.colors.map((color, idx) => {
const fieldProps = {
key: `${idx}`,
color,
/*onClick: () => { // PROBLEM HERE
this.props.colorPicked(color);
}*/
};
return <ColorField { ...fieldProps}/>;
});
return (
<div className="bla-picker">
<div>{`Refresh seed: ${this.props.seed}`}</div>
{fields}
< /div>
);
}
}
class Layout extends React.PureComponent {
constructor(props, ctx) {
super(props, ctx);
this.state = {
seed: 1
};
}
render() {
const pickerProps = {
colors: ['#f00', '#0f0', '#00f'],
colorPicked: (color) => {
console.log(`Color picked: ${color}`);
},
seed: this.state.seed
};
return (
<div>
<div
className="bla-button"
onClick = {this.btnClicked}
>
{'Click Me'}
</div>
<ColorPicker { ...pickerProps} />
</div>
);
}
btnClicked = () => {
this.setState({ seed: this.state.seed + 1 });
};
};
ReactDOM.render( <
Layout / > ,
document.getElementById("react")
);
.bla-button {
background-color: #aaa;
padding: 8px;
margin-bottom: 8px;
}
.bla-picker {}
.bla-field {
width: 32px;
height: 32px;
}
<div id="react">
</div>
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
As long as onClick remains commented out, only ColorPicker is re-rendered when the seed changes (see output from console.log). As soon as onClick is put in, all the ColorFields are re-rendered, too.
You can implement shouldComponentUpdate in your ColorField component like:
class ColorField extends React.Component {
shouldComponentUpdate(nextProps) {
return this.props.color !== nextProps.color;
}
render(){
const { color, onClick } = this.props;
console.log('Color re-rendered');
return (
<div
className="color"
onClick={onClick}
style={{
backgroundColor: color,
height: '50px',
width: '50px',
}}
/>
)
}
}
Example here
Be attentive as in the first solution we can use just React.Component because we implement shouldComponentUpdate by ourselves.
I'm experimenting with React and I'm trying to create a Search to filter a list of items. I have two components, the main one displaying the list of items which calls the Search component.
I have an onChange function that sets the term in the state as the input value and then calls searchItems from the main component to filter the list of items. For some reason in searchItems, this.state is undefined. I thought adding bind to onInputChange in the Search component would sort it out but it did not make any difference. Maybe there's something I'm missing.
Main Component
import React, { Component } from 'react';
import _ from 'lodash';
import Search from './search';
class Items extends Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
items: []
};
}
componentDidMount() {
fetch("[url].json")
.then(res => res.json())
.then(
(result) => {
this.setState({
isLoaded: true,
items: result
});
}
),
(error) => {
this.setState({
isLoaded: true,
error
})
}
}
searchItems(term) {
const { items } = this.state;
const filtered = _.filter(items, function(item) {
return item.Name.indexOf(term) > -1;
});
this.setState({ items: filtered });
}
render() {
const { error, isLoaded, items } = this.state;
if (error) {
return <div>Error: {error.message}</div>;
}
else if (!isLoaded) {
return <div>Loading...</div>;
}
else {
return (
<div>
<Search onSearch={this.searchItems}/>
<ul>
{items.map(item => (
<li key={item.GameId}>
{item.Name}
</li>
))}
</ul>
</div>
)
}
}
}
export default Items;
Search Component
import React, { Component } from 'react';
class Search extends Component {
constructor(props) {
super(props);
this.state = {
term: ''
};
}
render() {
return (
<div>
<input type="text" placeholder="Search" value={this.state.term} onChange={event => this.onInputChange(event.target.value)} />
</div>
);
}
onInputChange(term) {
this.setState({ term });
this.props.onSearch(term);
}
}
export default Search;
You didn't bind searchItems() in the Items component.
Try changing it to an arrow function:
searchItems = () => {
// blah
}
or otherwise binding it in the constructor():
constructor() {
// blah
this.searchItems = this.searchItems.bind(this);
}
or when you call it.
You can read more about this here.
I am struggling with successfully removing component on clicking in button. I found similar topics on the internet however, most of them describe how to do it if everything is rendered in the same component. In my case I fire the function to delete in the child component and pass this information to parent so the state can be changed. However I have no idea how to lift up the index of particular component and this is causing a problem - I believe.
There is a code
PARENT COMPONENT
export class BroadcastForm extends React.Component {
constructor (props) {
super(props)
this.state = {
numberOfComponents: [],
textMessage: ''
}
this.UnmountComponent = this.UnmountComponent.bind(this)
this.MountComponent = this.MountComponent.bind(this)
this.handleTextChange = this.handleTextChange.bind(this)
}
MountComponent () {
const numberOfComponents = this.state.numberOfComponents
this.setState({
numberOfComponents: numberOfComponents.concat(
<BroadcastTextMessageForm key={numberOfComponents.length} selectedFanpage={this.props.selectedFanpage}
components={this.state.numberOfComponents}
onTextChange={this.handleTextChange} dismissComponent={this.UnmountComponent} />)
})
}
UnmountComponent (index) {
this.setState({
numberOfComponents: this.state.numberOfComponents.filter(function (e, i) {
return i !== index
})
})
}
handleTextChange (textMessage) {
this.setState({textMessage})
}
render () {
console.log(this.state)
let components = this.state.numberOfComponents
for (let i = 0; i < components; i++) {
components.push(<BroadcastTextMessageForm key={i} />)
}
return (
<div>
<BroadcastPreferencesForm selectedFanpage={this.props.selectedFanpage}
addComponent={this.MountComponent}
textMessage={this.state.textMessage} />
{this.state.numberOfComponents.map(function (component) {
return component
})}
</div>
)
}
}
export default withRouter(createContainer(props => ({
...props
}), BroadcastForm))
CHILD COMPONENT
import React from 'react'
import { createContainer } from 'react-meteor-data'
import { withRouter } from 'react-router'
import { BroadcastFormSceleton } from './BroadcastForm'
import './BroadcastTextMessageForm.scss'
export class BroadcastTextMessageForm extends React.Component {
constructor (props) {
super(props)
this.handleChange = this.handleChange.bind(this)
this.unmountComponent = this.unmountComponent.bind(this)
}
handleChange (e) {
this.props.onTextChange(e.target.value)
}
unmountComponent (id) {
this.props.dismissComponent(id)
}
render () {
console.log(this.props, this.state)
const textMessage = this.props.textMessage
return (
<BroadcastFormSceleton>
<div className='textarea-container p-3'>
<textarea id='broadcast-message' className='form-control' value={textMessage}
onChange={this.handleChange} />
</div>
<div className='float-right'>
<button type='button'
onClick={this.unmountComponent}
className='btn btn-danger btn-outline-danger button-danger btn-small mr-3 mt-3'>
DELETE
</button>
</div>
</BroadcastFormSceleton>
)
}
}
export default withRouter(createContainer(props => ({
...props
}), BroadcastTextMessageForm))
I am having problem with access correct component and delete it by changing state. Any thoughts how to achieve it?
Please fix the following issues in your code.
Do not mutate the state of the component. Use setState to immutably change the state.
Do not use array index as the key for your component. Try to use an id field which is unique for the component. This will also help with identifying the component that you would need to unmount.
Try something like this. As mentioned before, you don't want to use array index as the key.
class ParentComponent extends React.Component {
constructor() {
this.state = {
// keep your data in state, as a plain object
textMessages: [
{
message: 'hello',
id: '2342334',
},
{
message: 'goodbye!',
id: '1254534',
},
]
};
this.handleDeleteMessage = this.handleDeleteMessage.bind(this);
}
handleDeleteMessage(messageId) {
// filter by Id, not index
this.setState({
textMessages: this.state.textMessages.filter(message => message.id !== messageId)
})
}
render() {
return (
<div>
{this.state.textMessages.map(message => (
// Use id for key. If your data doesn't come with unique ids, generate them.
<ChildComponent
key={message.id}
message={message}
handleDeleteMessage={this.handleDeleteMessage}
/>
))}
</div>
)
}
}
function ChildComponent({message, handleDeleteMessage}) {
function handleClick() {
handleDeleteMessage(message.id)
}
return (
<div>
{message.message}
<button
onClick={handleClick}
>
Delete
</button>
</div>
);
}