I do sorting on reactjs, I can’t understand how to redraw all child components so that only one selected remains active, I can update the current one, but the others do not change. Here is the code for an example. Can anyone help / explain how to do it right?
nodejs, webpack, last reactjs
App.js
import React, { Component } from "react";
import Parent from "./Parent";
class App extends Component {
render() {
return(
<Parent />
)
}
}
export default App;
Parent.js
import React, { Component } from "react";
import Child from "./Child";
class Parent extends Component {
constructor(props) {
super(props);
this.state = {
popularity: {"sorting": "desc", "active": true},
rating: {"sorting": "desc", "active": false},
reviews_count: {"sorting": "desc", "active": false},
};
}
updateFilters = () => {
// ??
};
render() {
return (
<div>
<Child type="popularity" sorting={this.state.popularity.sorting} active={this.state.popularity.active} updateFilters={this.updateFilters} />
<Child type="rating" sorting={this.state.rating.sorting} active={this.state.rating.active} updateFilters={this.updateFilters} />
<Child type="reviews_count" sorting={this.state.reviews_count.sorting} active={this.state.reviews_count.active} updateFilters={this.updateFilters} />
</div>
)
}
}
export default Parent;
Child.js
import React, { Component } from "react";
class Child extends Component {
handleClick = () => {
this.props.updateFilters();
};
render() {
let activeStr = "";
if (this.props.active) {
activeStr = "active"
} else {
activeStr = "inactive";
}
return(
<div onClick={() => this.handleClick}>
{this.props.type} {activeStr} {this.props.sorting}
</div>
);
}
}
export default Child;
Assuming you are trying to set the active flag for a clicked Type to true and also set all the other types to false.
<div onClick={() => this.handleClick}> this isn't correct, as you aren't invoking the function. This could be corrected to:
<div onClick={() => this.handleClick()}>
Then you can update handleClick to pass the Type:
handleClick = () => {
this.props.updateFilters(this.props.type);
};
OR
You could ignore that handleClick and call the prop function:
<div onClick={() => this.props.updateFilters(this.props.type)}>
Once you have passed the Type back into the updateFilters, we can simply iterate over the previous State Properties, setting all Types' Active Flag to false. However, if the Key matches the Type which was clicked, we set it to true.
updateFilters = type => {
this.setState(prevState => {
return Object.keys(prevState).reduce(
(result, key) => ({
...result,
[key]: { ...prevState[key], active: key === type }
}),
{}
);
});
};
Your Child component could be heavily refactored into a Pure Functional Component, making it a lot simpler:
const Child = ({ type, active, updateFilters, sorting }) => (
<div onClick={() => updateFilters(type)}>
{type} {active ? "active" : "inactive"} {sorting}
</div>
);
Work solution:
https://codesandbox.io/s/4j83nry569
Related
I want to create a HOC that have a event trigger when a certain key is pressed. When this key is pressed it should provide an event to the parent component. In this case, the key is "#".
Child HOC
import React, { Component } from 'react';
const withMention = WrappedComponent => {
return class extends Component {
state = {
mentionStart: false,
textInput: '',
selection: 0,
};
handleOnKeyPress = key => {
if (key === '#') {
this.setState({ mentionStart: true });
}
};
render() {
const { onMentionStart } = this.state;
return (
<WrappedComponent
onChangeText={text => {
this.setState({ textInput: text });
}}
onKeyPress={event => this.handleOnKeyPress(event.nativeEvent.key)}
onSelectionChange={event =>
this.setState({ selection: event.nativeEvent.selection })
}
onMentionStart={onMentionStart}
{...this.props}
/>
);
}
};
};
export default withMention;
Parent component
const UserComment = withMention(TextInput);
<UserComment onMentionStart={(event) => console.log(event)} />
I know the implementation is wrong, because whenever I assign a function to onMentionStart prop of child component in parent, child's function is overridden by parent. In this case, how to create a custom event trigger from child component and pass event into it so that the parent can use it accordingly?
I actually solved it by removing onMentionStart prop from HOC and passed onMentionStart function from parent to child as a callback, handled it in onKeyPress handler function.
import React, { Component } from 'react';
const withMention = WrappedComponent => {
return class extends Component {
state = {
mentionStart: false,
textInput: '',
selection: 0,
};
handleOnKeyPress = key => {
if (key === '#') {
this.setState({ mentionStart: true }, () =>
this.props.onMentionStart(this.state.mentionStart),
);
}
};
render() {
return (
<WrappedComponent
onChangeText={text => {
this.setState({ textInput: text });
}}
onKeyPress={event => this.handleOnKeyPress(event.nativeEvent.key)}
onSelectionChange={event =>
this.setState({ selection: event.nativeEvent.selection })
}
{...this.props}
/>
);
}
};
};
export default withMention;
How can I change the value of an array from inside a map function using button onClick event. In the following example I defined and array name visitMyArray that has three objects and initially the value of visited key is set as false. Using map function I render the all the location inside a paragraph tag. There will be a button rendered for each paragraph. Is it possible to change the value of the visited from false to true if possible how can I do it.
import React from "react";
import {Button} from 'react-bootstrap';
class VisitComponent extends React.Component {
render(){
let visitmyArray = [
{
location:"Indiana",
visited:false
},
{
location:"Illinoi",
visited:false
},
{
location:"Ohio",
visited:false
}
]
return(
<div>
{visitmyArray.map((visitedArray, index) => {
<div key={index}>
<p>{visitedArray.location}</p>
<Button onClick={"Change the visited value from 'false' to 'true' for this object value"}>Continue</Button>
</div>
)})}
</div>
}
}
export default VisitComponent
You can set the visited property to true for each item on the map. Your onClick now would be
onClick={() => {visitedArray.visited = true}}
Using state, it might look something like this:
import React from "react";
import {Button} from 'react-bootstrap';
class VisitComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
visitmyArray: [
{
location:"Indiana",
visited:false
},
{
location:"Illinoi",
visited:false
},
{
location:"Ohio",
visited:false
}
]
};
this.toggleVisited = this.toggleVisited.bind(this);
}
toggleVisited(location) {
return ev => {
var locations = this.state.visitmyArray.slice(0);
var loc = locations.find(a => a.location === location);
loc.visited = !loc.visited;
this.setState({visitmyArray:locations})
}
}
render(){
let {visitmyArray} = this.state;
return(
<div>
{visitmyArray.map((visitedArray, index) => (
<div key={index}>
<p>{visitedArray.location}</p>
<button className={visitedArray.visited ? "visited" : ""} onClick={this.toggleVisited(visitedArray.location)}>Continue</button>
</div>
))}
</div>
)
}
}
export default VisitComponent
You can define onClick as:
onClick = {() => {
visitmyArray[index].visited = true
}
}
I don't know your use case, but you shouldn't be defining the 'visitmyArray' in the render function. Every time the component renders, it will be redefined, so you should define it elsewhere. For instance,
let visitmyArray = [
{
location:"Indiana",
visited:false
},
{
location:"Illinoi",
visited:false
},
{
location:"Ohio",
visited:false
}
]
class VisitComponent extends React.Component {
render() {...}
}
If you want to listen to changes made to the array, you should define it as part of the component's state, like this:
let visitmyArray = [
{
location:"Indiana",
visited:false
},
{
location:"Illinoi",
visited:false
},
{
location:"Ohio",
visited:false
}
]
class VisitComponent extends React.Component {
constructor() {
super()
this.state = {
array: [...]
}
}
render() {...}
}
You should change the onClick to use the state's array to create a new array, and then use setState to actually modify it.
after let visitmyArray and before return( add:
markLocationAsVisited = (locationIndex) => {
this.visitmyArray[locationIndex].visited = true
}
and the in Button:
<Button onClick={() => markLocationAsVisited(index)}>
I'm having troubles updating the header class so it updates it's className whenever displaySection() is called. I know that the parent state changes, because the console log done in displaySection() registers the this.state.headerVisible changes but nothing in my children component changes, i don't know what I'm missing, I've been trying different solutions for some hours and I just can't figure it out what i'm doing wrong, the header headerVisible value stays as TRUE instead of changing when the state changes.
I don't get any error code in the console, it's just that the prop headerVisible from the children Header doesn't get updated on it's parent state changes.
Thank you!
class IndexPage extends React.Component {
constructor(props) {
super(props)
this.state = {
section: "",
headerVisible: true,
}
this.displaySection = this.displaySection.bind(this)
}
displaySection(sectionSelected) {
this.setState({ section: sectionSelected }, () => {
this.sectionRef.current.changeSection(this.state.section)
})
setTimeout(() => {
this.setState({
headerVisible: !this.state.headerVisible,
})
}, 325)
setTimeout(()=>{
console.log('this.state', this.state)
},500)
}
render() {
return (
<Layout>
<Header selectSection={this.displaySection} headerVisible={this.state.headerVisible} />
</Layout>
)
}
}
const Header = props => (
<header className={props.headerVisible ? 'visible' : 'invisible'}>
<div className="navbar-item column is-size-7-mobile is-size-5-tablet is-uppercase has-text-weight-semibold">
<span onClick={() => { this.props.selectSection("projects")}}>
{" "}
Projects
</span>
</header>
)
There seemed to be a couple of issues with your example code:
Missing closing div in Header
Using this.props instead of props in onclick in span in Header
The below minimal example seems to work. I had to remove your call to this.sectionRef.current.changeSection(this.state.section) as I didn't know what sectionRef was supposed to be because it's not in your example.
class IndexPage extends React.Component {
constructor(props) {
super(props)
this.state = {
section: "",
headerVisible: true,
}
this.displaySection = this.displaySection.bind(this)
}
displaySection(sectionSelected) {
this.setState({ section: sectionSelected })
setTimeout(() => {
this.setState({
headerVisible: !this.state.headerVisible,
})
}, 325)
setTimeout(()=>{
console.log('this.state', this.state)
},500)
}
render() {
return (
<div>
<Header selectSection={this.displaySection} headerVisible={this.state.headerVisible} />
</div>
)
}
}
const Header = props => (
<header className={props.headerVisible ? 'visible' : 'invisible'}>
<div className="navbar-item column is-size-7-mobile is-size-5-tablet is-uppercase has-text-weight-semibold">
<span onClick={() => { props.selectSection("projects")}}>
{" "}
Projects
</span>
</div>
</header>
)
ReactDOM.render(
<IndexPage />,
document.getElementsByTagName('body')[0]
);
.visible {
opacity: 1
}
.invisible {
opacity: 0
}
<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>
There is a markup error in your code in Header component - div tag is not closed.
Also, I suppose, you remove some code to make example easy, and there is artifact of this.sectionRef.current.changeSection(this.state.section) cause this.sectionRef is not defined.
As #Felix Kling said, when you change the state of the component depending on the previous state use function prevState => ({key: !prevState.key})
Any way here is a working example of what you trying to achieve:
// #flow
import * as React from "react";
import Header from "./Header";
type
Properties = {};
type
State = {
section: string,
headerVisible: boolean,
};
class IndexPage extends React.Component<Properties, State> {
static defaultProps = {};
state = {};
constructor(props) {
super(props);
this.state = {
section: "",
headerVisible: true,
};
this.displaySection = this.displaySection.bind(this)
}
displaySection(sectionSelected) {
setTimeout(
() => this.setState(
prevState => ({
section: sectionSelected,
headerVisible: !prevState.headerVisible
}),
() => console.log("Debug log: \n", this.state)
),
325
);
}
render(): React.Node {
const {section, headerVisible} = this.state;
return (
<React.Fragment>
<Header selectSection={this.displaySection} headerVisible={headerVisible} />
<br/>
<div>{`IndexPage state: headerVisible - ${headerVisible} / section - ${section}`}</div>
</React.Fragment>
)
}
}
export default IndexPage;
and Header component
// #flow
import * as React from "react";
type Properties = {
headerVisible: boolean,
selectSection: (section: string) => void
};
const ComponentName = ({headerVisible, selectSection}: Properties): React.Node => {
const headerRef = React.useRef(null);
return (
<React.Fragment>
<header ref={headerRef} className={headerVisible ? 'visible' : 'invisible'}>
<div className="navbar-item column is-size-7-mobile is-size-5-tablet is-uppercase has-text-weight-semibold">
<span onClick={() => selectSection("projects")}>Projects</span>
</div>
</header>
<br/>
<div>Header class name: {headerRef.current && headerRef.current.className}</div>
</React.Fragment>
);
};
export default ComponentName;
I have a simple compound component with a bunch of static subcomponents:
// #flow
import React, { Component, Children } from 'react';
type Props = {
children: React.ChildrenArray<React.Node> | React.Node,
}
class Toggle extends Component<Props> {
static On = props => (props.on ? props.children : null);
static Off = props => (props.on ? null : props.children);
static Button = props => (
<button
onClick={props.toggle}
type="button"
style={{ display: 'inline-block' }}
>
<pre>{JSON.stringify(props.on, null, 2)}</pre>
</button>
);
state = { on: false }
toggle = () => {
this.setState(
({ on }) => ({ on: !on }),
// maybe this.props.someCallback
() => console.log(this.state.on),
);
}
render() {
return Children.map(
this.props.children,
childElem => React.cloneElement(childElem, {
on: this.state.on,
toggle: this.toggle,
}),
);
}
}
export default Toggle;
The warning happens when I try to put some other elements into Toggle children scope.
For example:
<Toggle>
<Toggle.On>On</Toggle.On>
<span /> <-- this is savage
<Toggle.Button />
<Toggle.Off>Off</Toggle.Off>
</Toggle>
Everything is working, but my flowtype warn me about this span like so:
Warning: Received `false` for a non-boolean attribute `on`.....
Warning: Invalid value for prop `toggle` on <span> tag....
How can I to pacify this nasty girl?
Thank you guys, I think, right solution is just check if type of mounted node is correct one, otherwise - just clone node with regular node props:
// #flow
import React, { Component, Children } from 'react';
type Props = {
children: React.ChildrenArray<React.Node> | React.Node,
}
class Toggle extends Component<Props> {
static On = props => (props.on ? props.children : null);
static Off = props => (props.on ? null : props.children);
static Button = props => (
<button
onClick={props.toggle}
type="button"
style={{ display: 'inline-block' }}
>
<pre>{JSON.stringify(props.on, null, 2)}</pre>
</button>
);
state = { on: false }
toggle = () => {
this.setState(
({ on }) => ({ on: !on }),
// maybe this.props.someCallback
() => console.log(this.state.on),
);
}
// Checking here
allowedTypes = ({ type }) => {
return [
(<Toggle.On />).type,
(<Toggle.Off />).type,
(<Toggle.Button />).type,
].includes(type);
}
render() {
return Children.map(
this.props.children,
(childElem) => {
const elemProps = this.allowedTypes(childElem) ? {
on: this.state.on,
toggle: this.toggle,
} : childElem.props;
return React.cloneElement(childElem, elemProps);
},
);
}
}
export default Toggle;
You can also do this, just having the components in a list and checking their type inside .map, putting on the custom props or otherwise just returning the original child.
const allowedTypes = [ToggleOn, ToggleOff, ToggleButton]
return React.Children.map(props.children, child => {
if (allowedTypes.includes(child.type)) {
return React.cloneElement(child, {on, toggle})
}
return child
})
}
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>
);
}