React: change button text while doing work - javascript

In my React application, I would like a button that normally says Save, changes its text to Saving... when clicked and changes it back to Save once saving is complete.
This is my first attempt:
import React from 'react';
import { Button } from 'react-bootstrap';
class SaveButton extends React.Component {
constructor(props) {
super(props);
this.state = { isSaving : false };
this.onClick = this.onClick.bind(this);
}
onClick() {
// DOES NOT WORK
this.setState({ isSaving : true });
this.props.onClick();
this.setState({ isSaving : false });
}
render() {
const { isWorking } = this.state;
return (
<Button bsStyle="primary"
onClick={isWorking ? null : this.onClick}>
{isWorking ? 'Saving...' : 'Save'}
</Button>
);
}
}
export default SaveButton;
This doesn't work because both setState and this.props.onClick() (passed in by the parent component) are executed asynchronously, so the calls return immediately and the change to state is probably lost in React optimization (and would probably be visible only for a few milliseconds anuway...).
I read up on component state and decided to lift up state to where it belongs, the parent component, the form whose changes the button saves (which in my app rather is a distant ancestor than a parent). So I removed isSaving from SaveButton, replaced
const { isWorking } = this.state;
by
const { isWorking } = this.props.isWorking;
and tried to set the state in the form parent component. However, this might be architecturally cleaner, but essentially only moved my problem elsewhere:
The actual saving functionality is done at a totally different location in the application. In order to trigger it, I pass onClick event handlers down the component tree as props; the call chain back up that tree upon a click on the button works. But how do I notify about completion of the action in the opposite direction, i.e. down the tree ?
My question: How do I notify the form component that maintains state that saving is complete ?
The form's parent component (which knows about save being complete) could use a ref, but there isn't only one of those forms, but a whole array.
Do I have to set up a list of refs, one for each form ?
Or would this be a case where forwarding refs is appropriate ?
Thank you very much for your consideration! :-)

This doesn't work because both setState and this.props.onClick()
(passed in by the parent component) are executed asynchronously, so
the calls return immediately and the change to state is probably lost
in React optimization
setState can take a callback to let you know once state has been updated, so you can have:
onClick() {
this.setState({ isSaving : true }, () => {
this.props.onClick();
this.setState({ isSaving : false });
});
}
If this.props.onClick() is also async, turn it into a promise:
onClick() {
this.setState({ isSaving : true }, () => {
this.props.onClick().then(() => {
this.setState({ isSaving : false });
});
});
}

can you see ?
https://developer.mozilla.org/tr/docs/Web/JavaScript/Reference/Global_Objects/Promise
parent component's onClick function
onClick(){
return new Promise(function(resolve,reject){
//example code
for(let i=0;i<100000;i++){
//or api call
}
resolve();
})
}
SaveButton component's onClick funtion
onClick(){
this.setState({ isSaving : true });
this.props.onClick().then(function(){
this.setState({ isSaving : false });
});
}

Here is your code that works :
import React from 'react';
import { Button } from 'react-bootstrap';
class SaveButton extends React.Component {
constructor() {
super();
this.state = {
isSaving: false
};
this.handleOnClick = this.handleOnClick.bind(this);
}
handleOnClick() {
this.setState(prevState => {
return {
isSaving: !prevState.isSaving
}
});
console.log("Clicked!",this.state.isSaving);
this.handleSave();
}
handleSave() {
// Do what ever and if true => set isSaving = false
setTimeout(()=>{
this.setState(prevState=>{
return {
isSaving: !prevState.isSaving
}
})
},5000)
}
render() {
const isWorking = this.state.isSaving;
return (
<Button
bsStyle="primary"
onClick={this.handleOnClick}
>
{isWorking ? 'Saving...' : 'Save'}
</Button>
);
}
}
export default SaveButton;
You need to change const isWorking instead of const {isWorking} and you need to handle save function in some manner.
In this example I used timeout of 5 second to simulate saving.

Related

react - force componentDidUpdate() when props are same value

In my component im trying to sync the received props with the current state in order to make it visible from outside (I know this is an anti-pattern, but I haven't figured out another solution to this yet. Im very open to suggestions!).
Anyways, this is what I've got:
export class PopupContainer extends React.Component {
state = {
show: false,
};
shouldComponentUpdate(nextProps, nextState) {
if (this.props.show === nextProps.show) return true;
return true;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// if popup shown, fade it out in 500ms
if (this.props.show !== prevProps.show)
this.setState({ show: this.props.show });
if (this.state.show) {
setTimeout(() => this.setState({ show: false }), 2000);
}
}
render() {
return <Popup {...{ ...this.props, show: this.state.show }} />;
}
}
And in my external component I'm rendering the container :
<PopupContainer
show={this.state.popup.show}
message={this.state.popup.message}
level={this.state.popup.level}
/>
Now when I initially set this.state.show to true it works, but every successive assignment which is also true without any false assignment inbetween doesn't work. How do I force componentdidUpdate() to fire anyways even if the props are the same value? shouldComponentUpdate() didn't seem to solve the problem.
Thank you!
Edit: I noticed that the render() method is only called in the parent element. It seems like as there is no change in properties for the child, react doesn't even bother rerendering the childern which somehow makes sense. But how can I force them to rerender anyways?
This is kind of a hack, but it works for me.
In the child class
Add a property to state in constructor - let's call it myChildTrigger, and set it to an empty string:
this.state = {
...
myChildTrigger: ''
}
then add this to componentDidUpdate:
componentDidUpdate(prevProps) {
if(this.state.myChildTrigger !== this.props.myParentTrigger) {
// Do what you want here
this.setState({myChildTrigger: this.props.myParentTrigger});
}
}
In the parent class
Add a myParentTrigger to state in constructor:
this.state = {
...
myParentTrigger: ''
}
In the render method, add it as a prop, like this:
<ChildClass ... myParentTrigger={this.state.myParentTrigger} />
Now you can trigger a call to componentDidUpdate to execute whatever is inside the if-statement, just by setting myParentTrigger to a new value, like:
this.setState({ myParentTrigger: this.state.myParentTrigger + 'a' });

How to not use setState inside render function in React

I have a complete running code, but it have a flaw. It is calling setState() from inside a render().
So, react throws the anti-pattern warning.
Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount
My logic is like this. In index.js parent component, i have code as below. The constructor() calls the graphs() with initial value, to display a graph. The user also have a form to specify the new value and submit the form. It runs the graphs() again with the new value and re-renders the graph.
import React, { Component } from 'react';
import FormComponent from './FormComponent';
import PieGraph from './PieGraph';
const initialval = '8998998998';
class Dist extends Component {
constructor() {
this.state = {
checkData: true,
theData: ''
};
this.graphs(initialval);
}
componentWillReceiveProps(nextProps) {
if (this.props.cost !== nextProps.cost) {
this.setState({
checkData: true
});
}
}
graphs(val) {
//Calls a redux action creator and goes through the redux process
this.props.init(val);
}
render() {
if (this.props.cost.length && this.state.checkData) {
const tmp = this.props.cost;
//some calculations
....
....
this.setState({
theData: tmp,
checkData: false
});
}
return (
<div>
<FormComponent onGpChange={recData => this.graphs(recData)} />
<PieGraph theData={this.state.theData} />
</div>
);
}
}
The FormComponent is an ordinary form with input field and a submit button like below. It sends the callback function to the Parent component, which triggers the graphs() and also componentWillReceiveProps.
handleFormSubmit = (e) => {
this.props.onGpChange(this.state.value);
e.preventdefaults();
}
The code is all working fine. Is there a better way to do it ? Without doing setState in render() ?
Never do setState in render. The reason you are not supposed to do that because for every setState your component will re render so doing setState in render will lead to infinite loop, which is not recommended.
checkData boolean variable is not needed. You can directly compare previous cost and current cost in componentWillReceiveProps, if they are not equal then assign cost to theData using setState. Refer below updated solution.
Also start using shouldComponentUpdate menthod in all statefull components to avoid unnecessary re-renderings. This is one best pratice and recommended method in every statefull component.
import React, { Component } from 'react';
import FormComponent from './FormComponent';
import PieGraph from './PieGraph';
const initialval = '8998998998';
class Dist extends Component {
constructor() {
this.state = {
theData: ''
};
this.graphs(initialval);
}
componentWillReceiveProps(nextProps) {
if (this.props.cost != nextProps.cost) {
this.setState({
theData: this.props.cost
});
}
}
shouldComponentUpdate(nextProps, nextState){
if(nextProps.cost !== this.props.cost){
return true;
}
return false;
}
graphs(val) {
//Calls a redux action creator and goes through the redux process
this.props.init(val);
}
render() {
return (
<div>
<FormComponent onGpChange={recData => this.graphs(recData)} />
{this.state.theData !== "" && <PieGraph theData={this.state.theData} />}
</div>
);
}
}
PS:- The above solution is for version React v15.
You should not use componentWillReceiveProps because in most recent versions it's UNSAFE and it won't work well with async rendering coming for React.
There are other ways!
static getDerivedStateFromProps(props, state)
getDerivedStateFromProps is invoked right before calling the render
method, both on the initial mount and on subsequent updates. It should
return an object to update the state, or null to update nothing.
So in your case
...component code
static getDerivedStateFromProps(props,state) {
if (this.props.cost == nextProps.cost) {
// null means no update to state
return null;
}
// return object to update the state
return { theData: this.props.cost };
}
... rest of code
You can also use memoization but in your case it's up to you to decide.
The link has one example where you can achieve the same result with memoization and getDerivedStateFromProps
For example updating a list (searching) after a prop changed
You could go from this:
static getDerivedStateFromProps(props, state) {
// Re-run the filter whenever the list array or filter text change.
// Note we need to store prevPropsList and prevFilterText to detect changes.
if (
props.list !== state.prevPropsList ||
state.prevFilterText !== state.filterText
) {
return {
prevPropsList: props.list,
prevFilterText: state.filterText,
filteredList: props.list.filter(item => item.text.includes(state.filterText))
};
}
return null;
}
to this:
import memoize from "memoize-one";
class Example extends Component {
// State only needs to hold the current filter text value:
state = { filterText: "" };
// Re-run the filter whenever the list array or filter text changes:
filter = memoize(
(list, filterText) => list.filter(item => item.text.includes(filterText))
);
handleChange = event => {
this.setState({ filterText: event.target.value });
};
render() {
// Calculate the latest filtered list. If these arguments haven't changed
// since the last render, `memoize-one` will reuse the last return value.
const filteredList = this.filter(this.props.list, this.state.filterText);
return (
<Fragment>
<input onChange={this.handleChange} value={this.state.filterText} />
<ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
</Fragment>
);
}
}

componentDidMount not being called after goBack navigation call

I've set up a StackNavigator which will fire a redux action to fetch data on componentDidMount, after navigating to another screen and going back to the previous screen, componentDidMount is no longer firing and I'm presented with a white screen. I've tried to reset the StackNavigator using StackActions.reset but that just leads to my other screens not mounting as well and in turn presenting an empty screen. Is there a way to force componentDidMount after this.props.navigation.goBack()?
Go Back Function
_goBack = () => {
this.props.navigation.goBack();
};
Navigation function to new screen
_showQuest = (questId, questTitle ) => {
this.props.navigation.navigate("Quest", {
questId,
questTitle,
});
};
Edit :
Hi , I'm still stuck on this matter and was wondering if the SO Community has a solution to force componentDidMount to be called after Navigation.goBack() is called or rather how to Unmount the component after this.props.navigation.navigate is called
The components are not removed when the navigation changes, so componentDidMount will only be called the first time it is rendered.
You could use this alternative approach instead:
class MyComponent extends React.Component {
state = {
isFocused: false
};
componentDidMount() {
this.subs = [
this.props.navigation.addListener("didFocus", () => this.setState({ isFocused: true })),
this.props.navigation.addListener("willBlur", () => this.setState({ isFocused: false }))
];
}
componentWillUnmount() {
this.subs.forEach(sub => sub.remove());
}
render() {
if (!this.state.isFocused) {
return null;
}
// ...
}
}
The didFocus event listener didn't work for me.
I found a different listener called focus which finally worked!
componentDidMount() {
const { navigation } = this.props;
this.focusListener = navigation.addListener('focus', () => {
// call your refresh method here
});
}
componentWillUnmount() {
// Remove the event listener
if (this.focusListener != null && this.focusListener.remove) {
this.focusListener.remove();
}
}

Rendering previous state instead of current value selected in dropdown

I wrote the dropdown component that passes a selected value back to parent via callback function. From there I would like to simply render the selected value below the dropdown. Instead I have rendered previous state. I have no idea why that works like that, could someone explain me my app's behaviour and maybe give a hint how to fix it? I don't even know where to look for the answers.
index.js
import React, { Component } from 'react';
import './App.css';
import { Dropdown } from './components/dropdown'
class App extends Component {
state = {
response: "",
currA: ""
};
componentDidMount() {
this.callApi()
.then(res => this.setState({ response: res.express }))
.catch(err => console.log(err));
}
callApi = async () => {
const response = await fetch('/main');
const body = await response.json();
if (response.status !== 200) throw Error(body.message);
return body;
};
calculateRate = (currA) => {
this.setState({currA: currA});
}
render() {
return (
<div className="App">
<div>
<Dropdown callbackFromParent={this.calculateRate}/>
</div>
<p>
{this.state.currA}
</p>
</div>
);
}
}
export default App;
dropdown.js
import React from 'react';
export class Dropdown extends React.Component {
constructor(props){
super(props);
this.state = {
list: [],
selected: ""
};
}
componentDidMount(){
fetch('https://api.fixer.io/latest')
.then(response => response.json())
.then(myJson => {
this.setState({ list: Object.keys(myJson.rates) });
});
}
change(event) {
this.setState({ selected: event.target.value });
this.props.callbackFromParent(this.state.selected);
}
render(){
var selectCurr = (curr) =>
<select
onChange={this.change.bind(this)}
value={this.state.currA}
>
{(this.state.list).map(x => <option>{x}</option>)}
</select>;
return (
<div>
{selectCurr()}
</div>
);
}
}
Since your setState() is not a synchronous call, it might be that your callback is firing before the state of your dropdown is actually modified. You could try using the callback on setState...
change(event) {
this.setState({
selected: event.target.value
}, () => {this.props.callbackFromParent(event.target.value)});
;
}
...Or if your parent component is the only thing that cares about the selected value (my guess from your snip), you don't need to update the dropdown state at all.
change(event) {
this.props.callbackFromParent(event.target.value;)
}
Good luck!
Documentation:
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.

Reactjs: How to fetch data to loaded before the component is mounted?

Something weird is happening, I've been reading the React docs and they talk about the lifecycle and how you can do somestuff before your component is rendered. I am trying, but everything I try is failing, always the component makes the render first and after calls componenWillMount, ..didMount, etc.. and after the call of those functions, the render happens again.
I need to load the data first in order to fill the state because I don't want initial state to be null, I want it with data since the initial rendering.
I am using Flux and Alt, here is the
action
#createActions(flux)
class GetDealersActions {
constructor () {
this.generateActions('dealerDataSuccess', 'dealerDataFail');
}
getDealers (data) {
const that = this;
that.dispatch();
axios.get(`${API_ENDPOINT}/get-dealers/get-dealers`)
.then(function success (response) {
console.log('success GetDealersActions');
that.actions.dealerDataSuccess({...response.data});
})
}
}
then the store
#createStore(flux)
class GetDealersStore {
constructor () {
this.state = {
dealerData : null,
};
}
#bind(GetDealersActions.dealerDataSuccess)
dealerDataSuccess (data) {
this.setState({
dealerData : data,
});
}
}
and the component
#connectToStores
export default class Dealers extends Component {
static propTypes = {
title : React.PropTypes.func,
}
static contextTypes = {
router : React.PropTypes.func,
}
constructor (props) {
super(props);
this.state = {
modal : false,
dealerData : this.props.dealerData,
}
}
componentWillMount () {
GetDealersActions.getDealers();
this.setState({
dealerData : this.props.dealerData.dealersData,
})
}
static getStores () {
return [ GetDealersStore ];
}
static getPropsFromStores () {
return {
...GetDealersStore.getState(),
}
}
render () {
return (<div>
<div style={Styles.mainCont}>
{!!this.props.dealerData ?
this.props.dealerData.dealersData.map((dealer) => {
return (<div>HERE I AM RENDERING WHAT I NEED</div>);
}) : <p>Loading . . .</p>
}
</div>
</div>
);
}
}
as you can see in the component part I have this
constructor (props) {
super(props);
this.state = {
modal : false,
dealerData : this.props.dealerData,
}
}
componentWillMount () {
GetDealersActions.getDealers();
this.setState({
dealerData : this.props.dealerData.dealersData,
})
}
which is telling me that dealerData is undefined or can not read property of null.
All I need is to know a technique where I can fetch the data before the initial renders occurs. So I can filled out the state and the start working with that data.
React does guarantee that state assignments in componentWillMount will take place before the first render. As you well stated in the comments:
Invoked once, both on the client and server, immediately before the initial rendering occurs. If you call setState within this method, render() will see the updated state and will be executed only once despite the state change.
However, the asynchronous actions requested there will not immediately update your store. Calling GetDealersActions.getDealers(); will issue that the store is updated with new content, but the response will only arrive later in the event queue. This means that this.props.dealersData does not change during the function and setState will attempt to read property "dealersData" of an undefined property. Regardless, the requested content cannot be visible at the first render.
My advice is the same as the one in a similar question. Preventing the component from rendering that content until it becomes available, as you did, is an appropriate thing to do in a program. Alternatively, render a loader while your "dealersData" hasn't arrived.
For solving your particular problem, remove that setState from componentWillMount. All should work well if your parent component is properly listening for changes and propagating them to the children's props.
componentWillMount () {
GetDealersActions.getDealers();
}
The best answer I use to receive data from server and display it
constructor(props){
super(props);
this.state = {
items2 : [{}],
isLoading: true
}
}
componentWillMount (){
axios({
method: 'get',
responseType: 'json',
url: '....',
})
.then(response => {
self.setState({
items2: response ,
isLoading: false
});
console.log("Asmaa Almadhoun *** : " + self.state.items2);
})
.catch(error => {
console.log("Error *** : " + error);
});
})}
render() {
return(
{ this.state.isLoading &&
<i className="fa fa-spinner fa-spin"></i>
}
{ !this.state.isLoading &&
//external component passing Server data to its classes
<TestDynamic items={this.state.items2}/>
}
) }

Categories