I have a question about rendering based on conditions in React. I have this button component here that renders a list of buttons based on an API list of "ranks". When you press a button it will update a users rank to the new one. The catch is that they cannot be deranked through the same process, so whenever a user is placed at a certain rank, they cannot select any button lower than the current rank value. It looks like so:
interface BProps {
data: Array<any>;
buttonAction: any; //action for parent component
rank: string;
rankId: number;
}
interface BState {
data: Array<any>;
rankId: integer;
rank: string;
}
export default class rankButtons extends React.Component<BProps, BState> {
router: any;
constructor(props: BProps) {
super(props)
this.state = {
data: [],
rankId: this.props.rankId,
rank: this.props.rank,
}
this.updateRank = this.updateRank.bind(this);
}
updateRank(element) {
const { id, value } = element.target;
let headers: any = {//calls headers};
var payload = {//payload};
//get facade
assignNewRank(payload, headers).then(()=>{
//call buttonAction function.
this.props.buttonAction(
id, value
)
}).catch((e: any)=>{})
this.pageUpdate()
}
pageUpdate(){
//window.location.reload()
}
getData = () => {
//Make API Call};
let payload: any = {}
getReferenceValues(payload, headers).then((res:any)=>{
this.setState({
data: res.data,
})
}).catch((e: any)=>{})
}
componentDidMount(){
this.getData();
this.setState({rank: this.props.rank}, () => {});
}
render() {
return(
<div>
<h5>User Rankings:</h5>
<div className='buttonContainer'>
{this.state.data.map((rank =>
<li key={rank.key}>
<button id={rank.key} value = {rank.text} onClick={this.updateRank} disabled={rank.key <= this.state.rankId ? true : false}>
{rank.text}
</button>
</li>
))}
</div>
</div>
)
}
}
Currently the functionality works, I am able to update a users rank whenever one of the buttons is pressed. However I have set the buttons to disable with: disabled={rank.key <= this.state.rankId ? true : false} The problem with this is that I don't see the buttons become disabled unless I refresh the page. I have set it so that when you press a button the entire page refreshes, which works but it feels sloppy.
I know that in react you usually have to update the state in order for it to re-render components, but I am not sure how to do this in a similar method to what I have here. In short, it works how I want it, but I need the buttons to become disabled once the value changes without having to refresh the page.
Your updateRank function doesn't update the components state, so rankId doesn't change, it seems only to update your server (?).
Either your container component has to pass the rankId as prop and remove it from component state so you reference it as this.props.rankId instead of this.state.rankId or you need to update your rank within your updateRank function.
Related
I need to load different API urls on different button click:
Here is the API Component:
const companiesUrl = "http://localhost:3005/local-companies-xxx";
class Companies extends Component {
state = {
companyDetails: [],
currentPage: 0
};
get items() {
const {currentPage, companiesDetails} = this.state;
return companiesDetails.slice(currentPage * 3, currentPage * 3 + 3)
}
changeCurrentPage = (i) => {
this.setState({
currentPage : i
})
};
componentDidMount() {
fetch(companiesUrl)
.then(data => data.json())
.then(info => this.setState({companiesDetails: info}))
.catch(error => console.error("error connecting to the api"));
}
[more code... removed because not really necessary to show right now]
Here is the button Component:
class HomeButton extends Component {
render() {
return (
<div className="button" onClick={this.props.passClick}>
<p>{this.props.buttonText}</p>
</div>
)
}
}
export default HomeButton;
And here is the Parent:
class WhomWeHelp extends Component {
state = {
count : 0
};
increment() {
this.setState({
count : this.state.count + 1
})
}
render() {
return (
<section>
<div>
<HomeButton buttonText="xxx" passClick={this.increment} />
<HomeButton buttonText="yyy" passClick={this.increment} />
<HomeButton buttonText="zzzz" passClick={this.increment} />
</div>
<div>
<Companies />
</div>
</section>
)
}
}
export default WhomWeHelp;
As you can see I tried to add an onClick={this.props.passClick} to the button Component and pass the click to the Parent with passClick={this.increment} so that it runs the method
increment() {
this.setState({
count : 0
})
}
Then I wanted to transfer this state to the API Component and load a different API Url per different button.
Button xxx should load the api with an xxx url, button yyy should load the yyy companies, and zzz the zzz companies.
However right now when I click the 3 buttons I get WhomWeHelp.jsx:13 Uncaught TypeError: Cannot read property 'setState' of undefined at increment...
Why I am getting such error? If instead of this.setstate, I put a console log inside increment() {} it works fine.
Also is there a better way to achieve what I am doing avoiding to pass the click from child to parent, and then to pass the state from parent to API child?
Thanks
If when you put a console.log inside increment, it works fine, then I would say your this is the problem and your this is probably undefined. I imagine it is a scope issue.
Try changing your increment function to an arrow function expression like this:
increment = () => {
this.setState((state) => ({
count: state.count + 1
}));
}
For the issue of sending different API requests for different button clicks, you can create another arrow function expression as the binding event and send a flag to determine which API should be hit for each button. For example:
<button onClick={() => this.increment("xxx")}>xxx</button>
Check this reproducible example out where every button does something different to update the state:
I have the following code that simply constructs blocks for our products and the selected state allows the component to be selected and unselected. How can I figure out which of these components are selected and limit the user to only selecting one at a time. This is ReactJS code
import React from 'react';
export default class singleTile extends React.Component{
constructor(props){
super(props);
this.title = this.props.title;
this.desc = this.props.desc;
this.svg = this.props.svg;
this.id = this.props.id;
this.state = {
selected: false
}
}
selectIndustry = (event) => {
console.log(event.currentTarget.id);
if(this.state.selected === false){
this.setState({
selected:true
})
}
else{
this.setState({
selected:false
})
}
}
render(){
return(
<div id={this.id} onClick={this.selectIndustry}className={this.state.selected ? 'activated': ''}>
<div className="icon-container" >
<div>
{/*?xml version="1.0" encoding="UTF-8"?*/}
{ this.props.svg }
</div>
</div>
<div className="text-container">
<h2>{this.title}</h2>
<span>{this.desc}</span>
</div>
</div>
)
}
}
You need to manage the state of the SingleTile components in the parent component. What i would do is pass two props to the SingleTile components. A onClick prop which accepts a function and a isSelected prop that accepts a boolean. Your parent component would look something like this.
IndustrySelector.js
import React from 'react';
const tileData = [{ id: 1, title: 'foo' }, { id: 2, title: 'bar' }];
class IndustrySelector extends Component {
constructor(props) {
super(props);
this.state = { selectedIndustry: null };
}
selectIndustry(id) {
this.setState({ selectedIndustry: id });
}
isIndustrySelected(id) {
return id === this.state.selectedIndustry;
}
render() {
return (
<div>
{tileData.map((data, key) => (
<SingleTile
key={key}
{...data}
onClick={() => this.selectIndustry(data.id)}
isSelected={this.isIndustrySelected(data.id)}
/>
))}
</div>
);
}
}
The way this works is as follows.
1. Triggering the onClick handler
When a user clicks on an element in SingleTile which triggers the function from the onClick prop, this.selectIndustry in the parent component will be called with the id from the SingleTile component.
Please note that in this example, the id is remembered through a
closure. You could also pass the id as an argument to the function of
the onClick prop.
2. Setting the state in the parent component
When this.selectIndustry is called it changes the selectedIndustry key of the parent component state.
3. Updating the isSelected values form the SIngleTile components
React will automatically re-render the SingleTile components when the state of the parent component changes. By calling this.isIndustrySelected with the id of the SingleTile component, we compare the id with the id that we have stored in the state. This will thus only be equal for the SingleTile that has been clicked for the last time.
Can you post your parent component code?
It's not so important, but you can save some time by using this ES6 feature:
constructor(props){
super(props);
const {title, desc, svg, id, state} = this.props;
this.state = {
selected: false
}
}
I have a page that contains a lot of components such as TextFields and comboboxes. When a button, assume Clean, is clicked, I want to refresh the current page to set each component with their default values and also set state to initial state. When I tried window.location.reload(), it refreshes the whole page and user needs to login again.
I tried forceUpdate() but it preserves the current state of the page.
I also tried, it might be absurd I am newbie, to push history as below,
this.props.history.push("/");
this.props.history.push("new-record");
But this didn't work. Page stayed same.
How can I handle this?
try something similar for state reset:
const initialState = { name: 'React' }
class App extends Component {
constructor() {
super();
this.state = initialState
}
resetState=()=>{
this.setState(initialState)
}
render() {
return (
<div>
<Hello name={this.state.name} />
<button
onClick={this.resetState}>reset</button>
</div>
);
}
}
Here is an example with code:
Codepen
There is a local state with defaultInputValue and currentValue:
this.state = {
defaultInputValue: defaultValue,
currentValue: defaultValue
}
setDefault function restore default values:
setDefault = () => {
this.setState({currentValue: this.state.defaultInputValue});
}
The problem I face is that it doesn't seem that componentDidMount is re-rendering my component, even though it is updating the state. Lot of code coming up, but it gives context to the issue I'm having. If I need to, I can upload screenshots of what is happening.
Here's the constructor:
export class Upload extends React.Component<RouteComponentProps<{}>, UploadTaggingOptions> {
constructor(props: any) {
super(props);
this.state = {
photographer: [
{ id: null, value: '', label: '' },
],
};
}
Here's component did mount:
componentDidMount {
//Fetch request for photographers from the db
fetch("http://localhost:49775/api/photographers")
.then(res => res.json())
.then((result) => {
var photographerData = this.state!.photographer;
var y = 0;
//Remove the empty object first and foremost. The list should now be totally empty
photographerData.shift();
//The loop to add the galleries to the galleryData array
for (var i in result) {
var id = result[i].id;
var value = result[i].firstname + ' ' + result[i].lastname;
var label = value;
var photographer = { "id": id, "value": value, "label": label };
photographerData.push(photographer);
y++;
}
this.setState({
isLoaded: true,
photographer: photographerData
});
},
(error) => {
this.setState({
isLoaded: true,
error
});
alert("Error loading options for the photographers. Refresh the page. If the error persists, please contact your administrator");
}
)
And finally Render:
public render() {
return <div>
<div className="photographers">
<p><b>Photographer:</b></p>
<DropDown options={this.state!.photographer} />
</div>
}
Just for clarity's sake, there are more components on the screen (hence the extra div for the dropdown component).
I'm not sure why, but the dropdown renders with the blank options I intitialize in the constructor, componentdidupdate does the fetch request AND updates the state to the data that was fetched, but I have to click the blank value in order to load those data values into the dropdown. It is almost like it re-renders after I change the selected value instead of on state change.
I've tried moving those requests into the constructor, but have the same problem. Perhaps
EDIT: Here's the code for the Dropdown component:
import * as React from 'react';
import Select from 'react-select';
const DropDown = (props: any) => {
return (
<div className="dropdown">
<Select
closeOnSelect={!(props.stayOpen)}
disabled={props.disabled}
options={props.options}
placeholder="Select an option..."
removeSelected={props.removeSelected}
simpleValue
value={props.selectedPhotographer}
searchable={true}
multi={true}
/>
</div>
)
}
export default DropDown;
From react official documentation:
Never mutate this.state directly, as calling setState() afterwards may replace the mutation you made. Treat this.state as if it were immutable.
But in your code you are mutating it, albeit via an assignment to another variable:
var photographerData = this.state!.photographer;
// this DOES mutate the original array.
photographerData.shift();
This can mess with Reacts batching update strategy and can cause delays.
If you do not need the data from original state, you can just do:
var photographerData = [];
window.onload = function() {
console.log('Testing');
let testArr1 = [1, 2, 3]
console.log('TestArr1 length before shift: ' + testArr1.length);
let testArr2 = testArr1;
testArr2.shift();
console.log('TestArr1 length after shift: ' + testArr1.length);
}
I have a list component that uses a prop to amend the subscription parameters for the purpose of limiting displayed results. This works ok, however I would like to prevent the table from entirely refreshing when I add an extra 5 results to the list. Is this possible? Currently I update the props via a method I pass down from the parent component. I've included this at the bottom of the code.
Any advice appreciated!
export default class ListingTable extends TrackerReact(React.Component) {
static propTypes = {
LimitProp: React.PropTypes.number,
}
static defaultProps = {
LimitProp: 5
}
constructor(props) {
super(props);
const subscription = Meteor.subscribe("allLists",{sort: {_id:-1}, limit: this.props.LimitProp})
this.state = {
listData: subscription,
}
}
componentWillReceiveProps(nextProps) {
if (this.props.LimitProp != nextProps.LimitProp) {
// Stop old subscription
this.state.eventsData.stop()
// Setup new subscription
const subscription = Meteor.subscribe("allLists",{sort: {_id:-1}, limit: nextProps.LimitProp})
this.setState({ listData: subscription })
}
}
...
render() {
...
<a className="item" onClick= {(event) => this.props.methodLoadMore(event)}>Load More</a>
...
}
//this method is in the parent component, onClick Method calls this
loadMore(event) {
event.stopImmediatePropagation
event.preventDefault()
var limit = this.state.listLimit;
limit = limit + 5
this.setState({listLimit: limit})
FlowRouter.setQueryParams({limit: limit})
}