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>
.
Related
I know how to handle this inside a function, but in my case none of those solutions works and I still get
this is undefined
The problem is I have a function inside render() method and I dont know how to handle it.
This is a part of my code
class Pagination extends React.Component {
constructor(props) {
super(props);
this.state = {
//states
}
this.changePage = this.changePage.bind(this);
}
changePage (event) {
// some codes
}
render() {
function PrevPage() {
return (
<li key="p-page" onClick={this.changePage}>
<
</li>
)
}
return (
<div>
<PrevPage />
...
</div>
)
}
}
I get the error at this line
<li key="n-page" onClick={this.changePage}>
I tried putting this line in constructor:
this.PrevPage= this.PrevPage.bind(this);
but PrevPage is not recognized by this.
Also if i convert PervPage to arrow function:
PrevPage = () => {
return (
<li key="p-page" onClick={this.changePage}>
<
</li>
)
}
I get this error:
'PrevPage' is not defined no-undef
I know I'm missing somthing but I cant figure out what
Why not just remove the function outside the render? Generally speaking you want to keep the render logic as clean as possible and your PrevPage component can really just be a normal method. Seems kind of superfluous to define a child-component within render, and then return it immediately.
class Pagination extends React.Component {
constructor(props) {
super(props);
this.state = {
//states
}
this.changePage = this.changePage.bind(this);
this.prevPage = this.prevPage.bind(this);
}
changePage (event) {
// some codes
}
prevPage() {
return (
<li key="p-page" onClick={this.changePage}>
<
</li>
)
}
render() {
return (
<div>
{this.prevPage()}
...
</div>
)
}
}
Or if you want, just create a brand-new component for PrevPage, and pass down the changePage handler as props. The changeHandler will still be bound to the Pagination component context.
PrevPage.js
const PrevPage = (props) => {
return (
<li key="p-page" onClick={props.changePage}>
<
</li>
)
}
export default PrevPage
Pagination.js
class Pagination extends React.Component {
constructor(props) {
super(props);
this.state = {
//states
}
this.changePage = this.changePage.bind(this);
}
changePage (event) {
// some codes
}
render() {
return (
<div>
<PrevPage changePage={this.changePage}/>
</div>
)
}
}
You have multiple options:
bind PrevPage inside render:
render() {
function PrevPage() {
return (
<li key="p-page" onClick={this.changePage}>
<
</li>
)
}
PrevPage = PrevPage.bind(this);
return (
<div>
<PrevPage />
...
</div>
)
}
}
Save this in a different variable:
render() {
const that = this;
function PrevPage() {
return (
<li key="p-page" onClick={that.changePage}>
<
</li>
)
}
return (
<div>
<PrevPage />
...
</div>
)
}
}
Your PrevPage should be a separate component. It is not a good pattern to define a new component in the render function.
You can define it as a function component:
const PrevPage => (props) {
return (
<li key="p-page" onClick={props.onChangePage}>
<
</li>
)
}
class Pagination extends React.Component {
constructor(props) {
super(props);
this.state = {
//states
}
this.changePage = this.changePage.bind(this);
}
changePage (event) {
// some codes
}
render() {
return (
<div>
<PrevPage onChangePage={this.changePage}/>
...
</div>
)
}
}
What you've defined is a private function inside render method, therefore your attempt to bind it in the constructor not works, you have 2 choices:
Convert PrevPage function to arrow-function.
class Pagination extends React.Component {
constructor(props) {
super(props);
this.state = {
//states
};
this.changePage = this.changePage.bind(this);
}
changePage(event) {
// some codes
}
render() {
const PrevPage = () => {
return (
<li key="p-page" onClick={this.changePage}>
<
</li>
);
};
return (
<div>
<PrevPage />
...
</div>
);
}
}
Make PrevPage function as a class method, and then bind it in the constructor as you did - this method will preform better, cause you won't make a closure each time render method is called.
class Pagination extends React.Component {
constructor(props) {
super(props);
this.state = {
//states
};
this.changePage = this.changePage.bind(this);
this.renderPrevPage = this.renderPrevPage.bind(this);
}
changePage(event) {
// some codes
}
render() {
return (
<div>
{this.renderPrevPage()}
...
</div>
);
}
renderPrevPage() {
return (
<li key="p-page" onClick={this.changePage}>
<
</li>
);
}
}
There are 2 issues in your code.
You are creating a function PrevPage in render method and calling it as a component instead you have to call it as a function.
You are not binding the method changePage correctly to OnClick.
This is how I will write this code.
render() {
function PrevPage() {
return (
<li key="p-page" onClick={ () => this.changePage }>
<
</li>
)
}
return (
<div>
{PrevPage()}
...
</div>
)
I will suggest you to write PrevPage function outside the render method and call it like this.
{this.PrevPage}
Hope it helps you.
You have to bind you function to contructor and call the function on your li tag as an arrow function and also put your PrevPage function outside render function like this:
class Pagination extends React.Component {
constructor(props) {
super(props);
this.state = {
//states
}
this.changePage = this.changePage.bind(this);
this.PrevPage = this.PrevPage.bind(this);
}
changePage (event) {
// some codes
}
PrevPage() {
return (
<li key="p-page" onClick={this.changePage}>
<
</li>
)
}
render() {
return (
<div>
{ this.PrevPage() }
...
</div>
)
}
}
Hope it helps
try
<li key="n-page" onClick={() => this.changePage()}>
change it to arrow function to get "this" instance like in below code,
changePage = (event) => {
/* here you can access to "this" instance
eg: this.setState({ pageName:'myPage' )}
*/
}
I want to create a react component instance and render it in a static place programmatically.
My use-case is that I open a sequence of dialogs in an unknown length and when I get a response from a dialog I open the next.
I want to do something like:
const DialogExample = () => ({ question, onAnswer }) =>
(<div>
{question}
<button onClick={onAnswer}>answer</button>
</div>);
class SomeComponent extends React.Component {
async start() {
const questions = await getSomeDynamicQuestions();
this.ask(questions);
}
ask(questions) {
if (questions.length === 0) {
// DONE.. (do something here)
return;
}
const current = questions.pop();
React.magicMethod(
// The component I want to append:
<DialogExample
question={current}
onAnswer={() => this.ask(questions)}
/>,
// Where I want to append it:
document.getElementsByTagName('body')[0]);
}
render() {
return (
<div>
<button onClick={this.start}>start</button>
</div>);
}
}
I know that's not very "react-like", and I guess the "right" way of doing it will be storing those questions in state and iterate over them in "someComponent" (or other) render function, but still, I think that this pattern can make sense in my specific need.
Sounds like a case for Portals. I'd recommend doing something like this:
class SomeComponent extends React.Component {
constructor(props) {
super(props);
this.body = document.getElementsByTagName('body')[0];
this.state = {
questions: [],
}
}
async start() {
const questions = await getSomeDynamicQuestions();
this.setState({ questions });
}
nextQuestion() {
this.setState(oldState => {
const [first, ...rest] = oldState.questions;
return { questions: rest };
})
}
render() {
const { questions } = this.state;
return (
<div>
<button onClick={this.start}>start</button>
{questions.length > 0 && ReactDOM.createPortal(
<DialogExample
question={questions[0]}
onAnswer={() => this.nextQuestion()}
/>,
this.body,
)}
</div>
);
}
}
I try to map an array and put click event on the array items. I know it's a bit different because of how JavaScript handles functions but I can't make it work. I get the error: Cannot read property 'saveInStorage' of undefined. Can anyone tell me what I'm doing wrong? Thanks in advance! Here is my code:
import React from "react";
const data = require('../data.json');
export default class Gebruikers extends React.Component {
constructor() {
super();
this.state = {
users: data.users
};
this.saveInStorage = this.saveInStorage.bind(this)
}
saveInStorage(e){
console.log("test");
}
renderUser(user, i) {
return(
<p key={i} onClick={this.saveInStorage(user)}>f</p>
);
}
render() {
return (
<div>
{
this.state.users.map(this.renderUser)
}
</div>
);
}
}
this is undefined in renderUser()
You need to bind this for renderUser() in your constructor.
Also, you are calling saveInStorage() every time the component is rendered, not just onClick, so you'll need to use an arrow function in renderUser
import React from "react";
const data = require('../data.json');
export default class Gebruikers extends React.Component {
constructor() {
super();
this.state = {
users: data.users
};
this.saveInStorage = this.saveInStorage.bind(this);
this.renderUser = this.renderUser.bind(this);
}
saveInStorage(e){
console.log("test");
}
renderUser(user, i) {
return(
<p key={i} onClick={() => this.saveInStorage(user)}>
);
}
render() {
return (
<div>
{
this.state.users.map(this.renderUser)
}
</div>
);
}
}
Instead of binding you can also use an arrow function (per mersocarlin's answer). The only reason an arrow function will also work is because "An arrow function does not have its own this; the this value of the enclosing execution context is used" (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions). The enclosing execution in your case is your render, where this is defined.
You need to make two changes to your code which are outlined below.
You are invoking the function when the component is rendered. To fix this update this line to the following
<p key={i} onClick={() => this.saveInStorage(user)}>
This means that the function will only be invoked when you click on the item.
You also need to bind the renderUser in your constructor or else use an arrow function.
this.renderUser = this.renderUser.bind(this);
See working example here.
Your onClick event handler is wrong.
Simply change it to:
onClick={() => this.saveInStorage(user)}
Don't forget to also bind renderUser in your constructor.
Alternatively, you can choose arrow function approach as they work the same as with bind:
class Gebruikers extends React.Component {
constructor(props) {
super(props)
this.state = {
users: [{ id: 1, name: 'user1' }, { id: 2, name: 'user2' }],
}
}
saveInStorage = (e) => {
alert("test")
}
renderUser = (user, i) => {
return(
<p key={i} onClick={() => this.saveInStorage(user)}>
{user.name}
</p>
)
}
render() {
return (
<div>{this.state.users.map(this.renderUser)}</div>
);
}
}
ReactDOM.render(
<Gebruikers />,
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>
Paul Fitzgeralds answer is the correct one, although I'd like to propose a different way of handling this, without all the binding issues.
import React from "react";
const data = require('../data.json');
export default class Gebruikers extends React.Component {
constructor() {
super();
this.state = {
users: data.users
};
}
saveInStorage = (e) => {
console.log("test");
};
render() {
return (
<div>
{this.state.users.map((user, i) => {
return (<p key={i} onClick={() => this.saveInStorage(user)}>f</p>);
})}
</div>
);
}
}
With saveInStorage = (e) => {}; you are binding the saveInStorage function to the this context of your class. When invoking saveInStorage you'll always have the (at least I guess so in this case) desired this context.
The renderUser function is basically redundant. If you return one line of JSX, you can easily do this inside your render function. I think it improves readability, since all your JSX is in one function.
You are not sending the parameters to this.renderUser
this.state.users.map((user, i) => this.renderUser(user, i))
Also your onClick function should be slightly changed. Here's the full code changed:
import React from "react";
const data = require('../data.json');
export default class Gebruikers extends React.Component {
constructor() {
super();
this.state = {
users: data.users
};
this.saveInStorage = this.saveInStorage.bind(this)
}
saveInStorage(e){
console.log("test");
}
renderUser(user, i) {
return(
<p key={i} onClick={() => this.saveInStorage(user)}>f</p>
);
}
render() {
return (
<div>
{
this.state.users.map((user, i) => this.renderUser(user, i))
}
</div>
);
}
}
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>
);
}
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>