React: how to load different api on different button click - reactjs

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:

Related

How to change state for individual buttons in React

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.

Changing state in React JS is not re-rendering parent

My react app is a multi-page form. It goes to next page after clicking 'Next'. Currently I have some text that should have a css class when current page is page 1, and when user goes to next page, the css class should be removed for that text (the text is still displayed for all pages).
My actual code is much larger so I'm only posting all the important parts(I think) that are required for this questions.
import ChildComponent from '....';
class Parent extends React.Component {
state = {
page: 1, //default start page
currentPageis1: true,
currentPageis2: false,
currentPageis3: false,
}
change = () => {
const = {page, currentPageis1} = this.state;
this.setState({
page: page + 1 //to go to next page
});
this.setState({
currentPageis1: !currentPageis1
});
}
showPage = () =>{
const {page, currentPageis1} = this.state;
if(page === 1)
return (<ChildComponent
change={this.change}
currentPageis1={currentPageis1}
/>)
}
render(){
return (
<p className={this.currentPageis1 ? '': 'some-css-class'}>Some Text</p>
<form>{this.showPage()}
)
}
}
class ChildComponent extends React.Component {
someFunction = e =>{
e.preventDefault();
this.props.change();
}
render(){
return (
<Button onClick={this.someFunction}>Next</Button>
)
}
}
Currently, when I click Next button, the currentPageis1 updates to false. I checked it using Firefox React extension. But it does not re-render the page. Which means "Some Text" still has the CSS class.
My guess is className={this.currentPageis1 ? '': 'css-class'} in Parent class is only being run once (when the page is first loaded). Do I have to use lifecycle method? How do I make react re-render everytime currentPageis1 is changed?
You are doing <p className={this.currentPageis1 ? '': 'some-css-class'}>Some Text</p>. In order to apply styles to only page 1, you should revert the values in your condition. When currentPageis1 is false '' value is picked up.
Also this.currentPageis1 is wrong. You should use state i.e. this.state.currentPageis1
Working demo
Like this
<p className={this.state.currentPageis1 ? "some-css-class" : ""}>
Some Text
</p>
To get your style to render, you'll need to add the props keyword.
Return Child component inside of Parent and pass the change method as
a prop
Also, updated your setState so you only call it once instead of twice
in the change method
class Parent extends React.Component {
state = {
page: 1, //default start page
currentPageis1: true,
currentPageis2: false,
currentPageis3: false,
}
change = () => {
const = {page, currentPageis1} = this.state;
this.setState({
...this.state,
page: page + 1,
currentPageis1: !currentPageis1
});
}
render(){
return (
<div>
<p className={this.props.currentPageis1 ? '': 'some-css-class'}>Some Text</p>
<Child change={this.change} />
</div>
)
}
}

How to change a component's state correctly from another component as a login method executes?

I have two components - a sign in form component that holds the form and handles login logic, and a progress bar similar to the one on top here in SO. I want to be able to show my progress bar fill up as the login logic executes if that makes sense, so as something is happening show the user an indication of loading. I've got the styling sorted I just need to understand how to correctly trigger the functions.
I'm new to React so my first thought was to define handleFillerStateMax() and handleFillerStateMin() within my ProgressBarComponent to perform the state changes. As the state changes it basically changes the width of the progress bar, it all works fine. But how do I call the functions from ProgressBarComponent as my Login component onSubmit logic executes? I've commented my ideas but they obviously don't work..
ProgressBarComponent:
class ProgressBarComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
percentage: 0
}
}
// the functions to change state
handleFillerStateMax = () => {
this.setState ({percentage: 100})
}
handleFillerStateMin = () => {
this.setState ({percentage: 0})
}
render () {
return (
<div>
<ProgressBar percentage={this.state.percentage}/>
</div>
)
}
}
Login component:
class SignInFormBase extends Component {
constructor(props) {
super(props);
this.state = {...INITIAL_STATE};
}
onSubmit = event => {
const {email, password} = this.state;
// ProgressBarComponent.handleFillerMax()????
this.props.firebase
.doSignInWithEmailAndPass(email,password)
.then(()=> {
this.setState({...INITIAL_STATE});
this.props.history.push('/');
//ProgressBarComponent.handleFillerMin()????
})
.catch(error => {
this.setState({error});
})
event.preventDefault();
}
Rephrase what you're doing. Not "setting the progress bar's progress" but "modifying the applications state such that the progress bar will re-render with new data".
Keep the current progress in the state of the parent of SignInFormBase and ProgressBarComponent, and pass it to ProgressBarComponent as a prop so it just renders what it is told. Unless there is some internal logic omitted from ProgressBar that handles its own progress update; is there?
Pass in a callback to SignInFormBase that it can call when it has new information to report: that is, replace ProgressBarComponent.handleFillerMax() with this.props.reportProgress(100) or some such thing. The callback should setState({progress: value}).
Now, when the SignInFormBase calls the reportProgress callback, it sets the state in the parent components. This state is passed in to ProgressBarComponent as a prop, so the fact that it changed will cause he progress bar to re-render.
Requested example for #2, something like the following untested code:
class App extends Component {
handleProgressUpdate(progress) {
this.setState({progress: progress});
}
render() {
return (
<MyRootElement>
<ProgressBar progress={this.state.progress} />
<LoginForm onProgressUpudate={(progress) => this.handleProgressUpdate(progress)} />
</MyRootElemen>
)
}
}
The simply call this.props.onProgressUpdate(value) from LoginForm whenever it has new information that should change the value.
In basic terms, this is the sort of structure to go for (using useState for brevity but it could of course be a class-based stateful component if you prefer):
const App = ()=> {
const [isLoggingIn, setIsLoggingIn] = useState(false)
const handleOnLoginStart = () => {
setIsLoggingIn(true)
}
const handleOnLoginSuccess = () => {
setIsLoggingIn(false)
}
<div>
<ProgressBar percentage={isLoggingIn?0:100}/>
<LoginForm onLoginStart={handleOnLogin} onLoginSuccess={handleOnLoginSuccess}/>
</div>
}
In your LoginForm you would have:
onSubmit = event => {
const {email, password} = this.state;
this.props.onLoginStart() // <-- call the callback
this.props.firebase
.doSignInWithEmailAndPass(email,password)
.then(()=> {
this.setState({...INITIAL_STATE});
this.props.history.push('/');
this.props.onLoginSuccess() // <-- call the callback
})
.catch(error => {
this.setState({error});
})
event.preventDefault();
}

React+redux get data from child component

I have creator, I mean step1 -next-> step2 -next-> ...
In my parent component I have buttons preview and next, steps content are render as child.
class MyCreator extends React.Component {
render() {
return (
<div>
{this.renderStep(this.props.step.id)}
</div>
<div>
<button>back</button>
<button>next</button>
</div>
);
}
}
In a step I have a component which has only two methods: getData, setData. This is a third party component (so I cannot change implementation).
When I click button next I want to getData from the current step. I mean call some generic method on each step child component, like leaveStep. Then leaveStep returns some data, which I will pass to redux action.
If I got it right, the ideal solution would be lifting state up to the Parent component, take a look at this part of the React documentation. But since you don't have control of your components and it may create you some problems to sync the states. Something like this will do the trick:
class Parent extends Component {
state = {
childData: null
}
getChildData = (data) => {
this.setState({
childData: data,
}, () => { console.log(this.state); });
}
render() {
return <Child setData={this.getChildData} />
}
}
class Child extends Component {
state = {
data: 'this is child data'
}
render() {
return <button onClick={() => this.props.setData(this.state.data)}>Next</button>;
}
}
But remember that this creates a duplicate state, breaking the single source of truth and can be very messy for large applications.

When is the render() called in a class that extends Component?

What refreshes the view in react or is the code always live displayed?
I have a function called removeAdmin and makeAdmin which adds and removes users as Admins and then when a user is an admin the render of Member component renders and admin shield logo. It works fine but I'm wondering whether render is being triggered each time I change the UI using a function or if render is live listening to changes in it's components?
class MemberList extends Component {
constructor(props) {
super(props)
this.state = {
members: [],
loading: false,
administrators: []
}
this.makeAdmin = this.makeAdmin.bind(this)
this.removeAdmin = this.removeAdmin.bind(this)
}
componentDidMount(){
this.setState({loading:true})
fetch('https://api.randomuser.me/?nat=US&results=12')
.then(response => response.json())
.then(json => json.results)
.then(members => this.setState({
members,
loading:false
}))
}
makeAdmin(email){
const administrators = [
...this.state.administrators,
email
]
this.setState({administrators})
}
removeAdmin(email){
const administrators = this.state.administrators.filter(
adminEmail => adminEmail !== email
)
this.setState({administrators})
}
render() {
const { members, loading } = this.state
return (
<div className="member-list">
<h1>Society Members</h1>
{
(loading) ?
<span> loading...</span>:
<span>{members.length} members</span>
}
{ (members.length)?
members.map(
(member, i) =>
<Member key={i}
// This admin prop is worked out by enumerating through the administrator
// array with some(). some() passes in the enumerators, checking whether
// the current member in members.map() exists in the administrators array
// and returns admin=true if so.
admin={this.state.administrators.some(
adminEmail => adminEmail === member.email
)}
name={member.name.first + '' + member.name.last}
email={member.email}
thumbnail={member.picture.thumbnail}
makeAdmin={this.makeAdmin}
removeAdmin={this.removeAdmin}/>
):
<span>Currently 0 members</span>
}
</div>
)
and the Member component:
class Member extends Component {
componentWillMount(){
this.style={
backgroundColor: 'grey'
}
}
render() {
const { name, thumbnail, email, admin, makeAdmin, removeAdmin } = this.props
return (
<div className="member" style={this.style}>
<h1>{ name } {(admin) ? <FaShield/> : null}</h1>
<div>
<img src={ thumbnail }/>
</div>
<div>
{
(admin)?
<Button title="Make Admin" onClick={() => removeAdmin(email) } color="#841584"> Remove Admin </Button>
:
<Button title="Make Admin" onClick={ () => makeAdmin(email) } color="#841584"> Make Admin </Button>
}
<a href={`mailto:${ email }`}><p> {email} </p></a>
</div>
</div>
)
}
}
export default Member
What triggers a new render on components is when the state changes or when receiving new properties.
There are two main objects that drive the render in each component, the this.props and the this.state. If any of this objects gets updated then the render method gets executed.
The this.props object gets updated whenever you send new properties to the childrens. this.state gets updated using the this.setState method.
That being said, it's really important to keep track of the properties you send to the children, as a rule of thumb I always recommend not using the spread operator to pass props to the children, for example:
<Parent>
<Child {...this.props} />
</Parent>
I'd avoid that pattern because if any of the props changes, than all props are sent to the child. Instead I recommend sending only what the children needs.
<Parent>
<Child some={this.props.value} />
</Parent>
You need to be very careful when you need to render your component, otherwise it's so easy to re-render everything! Which will lead to performance issues.
It depends on what you define your component render method to be.
It can change based on the state or props that you give it.
Less frequent, but you can check out shouldComponentUpdate as it allows you to overwrite the method to give it more “smarts” if you need the performance boost.

Resources