for example I have this React component which simply does loading text. I have a problem with refactoring .bind(this) to es6 syntax.
var Loading = React.createClass({
getInitialState:() => {
this.originalText = 'Loading';
return {
text: 'Loading'
}
},
componentDidMount:function() {
let stopper = this.originalText + '...' ;
this.interval = setInterval(function(){
if(this.state.text === stopper) {
this.setState({
text:this.originalText
})
}else {
this.setState({
text: this.state.text + '.'
})
}
}.bind(this), 300)
},
render: function () {
return (
<div style={styles.container}>
<p style={styles.content}>{this.state.text}</p>
</div>
)
}
});
here I want to refactor
}.bind(this), 300)
to ES6 syntax. What would be solution.
You can replace:
this.interval = setInterval(function(){
/* ... */
}.bind(this), 300)
with:
this.interval = setInterval( () => {
/* ... */
}, 300)
That's because arrow functions automatically binds. BTW, I refactored all your component code to ES6:
class Loading extends React.Component {
constructor(props) {
super(props)
this.originalText = 'Loading'
this.state = {
text: 'Loading'
}
}
componentDidMount() {
let stopper = this.originalText + '...' ;
this.interval = setInterval( () => {
if(this.state.text === stopper) {
this.setState({
text:this.originalText
})
} else {
this.setState({
text: this.state.text + '.'
})
}
}, 300)
}
render() {
return (
<div style={styles.container}>
<p style={styles.content}>{this.state.text}</p>
</div>
)
}
}
Fiddle here: https://jsfiddle.net/mrlew/jgtyetwu/
Still }.bind(this), 300). ES6 is backwards compatible.
You could also use an arrow function (as you do to define getInitialState).
function () { ... }.bind(this) is what arrow function is supposed to do.
It is
this.interval = setInterval(() => { ... }, 300)
Related
I saw there are already answered questions on how to add spinners during fetch requests.
However what I need is to stop showing the animation when the animation completes. The animation completes after the timeout is reached.
Also I have a best practice question.
It's a good practice to empty the resources on componentWillUnmount and clear there the timeout. In the code below I clear the timeout in a if condition, because it has to stop as the height of the element reaches the right level.
Is that ok as I did it? If now, how should it look like to have the same functionality in the componentWillUnmount lifecycle phase?
Here is the animation Component:
class Thermometer extends Component {
state = {
termFill : 0
};
componentDidMount() {
const interval = setInterval(() => {
this.setState({
termFill: this.state.termFill + 10
});
if (this.state.termFill === 110) {
window.clearInterval(interval);
}
}, 200)
}
render() {
const styles = {
height: `${this.state.termFill}px`
};
if (this.state.termFill < 100) {
return (
<section>
<div id="therm-fill" style={styles} />
[MORE CODE - SHORTENED FOR EASIER READING]
)
}
};
And here is the Component that has to appear after the animation disappears.
The steps are like this:
A user enter and uses this tool
The user clicks "calculate"
The animation appears instead or on top of the tool
When the animation completes, the animation Component disappears and the tool
is once again visible with its updated state (results of the
calculation).
class DiagnoseTool extends Component {
state = {
[OTHER STATES REMOVED TO KEEP THE CODE SHORTER]
wasBtnClicked: false
};
[OTHER RADIO AND CHECKBOX HANDLERS REMOVED TO KEEP THE CODE SHORTER]
onButtonClick = e => {
e.preventDefault();
this.calculate();
this.setState({
wasBtnClicked: true
})
};
addResult = () => {
const resultColor = {
backgroundColor: "orange"
};
let theResult;
if (this..... [CODE REMOVED TO HAVE THE CODE SHORTER]
return theResult;
};
calculate = () => {
let counter = 0;
let radiocounter = 0;
Object.keys(this.state).filter(el => ['cough', 'nodes', 'temperature', 'tonsillarex'].includes(el)).forEach(key => {
// console.log(this.state[key]);
if (this.state[key] === true) {
counter += 1;
}
});
if (this.state.radioAge === "age14") {
radiocounter++
} else if (this.state.radioAge === "age45") {
radiocounter--
}
if (this.state.radioAge !== "") {
this.setState({
isDisabled: false
})
}
this.setState({
points: counter + radiocounter
});
};
render() {
const {cough, nodes, temperature, tonsillarex, radioAge, wasBtnClicked} = this.state;
return (
<Container>
<BackArrow />
[JSX REMOVED TO KEEP THE CODE SHORTER]
<div className="resultbox">
{
(wasBtnClicked) && this.addResult()
}
</div>
</div>
[HERE IS THE BUTTON]
<button
style={{height: "40px", width: "150px", cursor:"pointer"}}
type="submit"
className="calculateBtn"
onClick={this.onButtonClick}
disabled={!radioAge}
>CALCULATE</button>
</Container>
Add a boolean to your state and set it to false, when the user clicks the button set it to true, after doing the calculation set it to false.
calculate = () => {
let counter = 0;
let radiocounter = 0;
this.setState({
isLoading: true // set is loading to true and show the spinner
})
Object.keys(this.state)
.filter(el =>
["cough", "nodes", "temperature", "tonsillarex"].includes(el)
)
.forEach(key => {
// console.log(this.state[key]);
if (this.state[key] === true) {
counter += 1;
}
});
if (this.state.radioAge === "age14") {
radiocounter++;
} else if (this.state.radioAge === "age45") {
radiocounter--;
}
if (this.state.radioAge !== "") {
this.setState({
isDisabled: false
});
}
this.setState({
points: counter + radiocounter,
isLoading: false // set it to false and display the results of the calculation
});
};
Example
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.0/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.21.1/babel.min.js"></script>
<div id="root"></div>
<script type="text/babel">
class App extends React.Component {
timer = null;
constructor() {
super();
this.state = {
result: '',
isLoading: false
};
}
showContent = () => { this.setState({ isLoading: false, result: `7 + 5 = ${7 + 5}` })}
calculate = () => {
this.setState({
isLoading: true,
result: ''
});
this.timer = setTimeout(this.showContent, 5000);
}
componentWillUnmount = () => {
clearTimeout(this.timer);
}
render() {
return (
<div>
<p>7 + 5</p>
<p>{this.state.result}</p>
{ this.state.isLoading
? <p>Calculating...</p>
: <button onClick={this.calculate}>Calculate</button>
}
</div>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
</script>
I want to call a form submit function after multiple tasks are completed.
The tasks can be completed in any order.
I tried solving it like this:
function callbackWhenCompleted(callback) {
let tasks = {
imageUploaded: false,
submitButtonClicked: false
};
function taskCompleted(taskName) {
tasks[taskName] = true;
if (Object.values(tasks).every(Boolean)) {
callback();
}
}
return taskCompleted;
}
class Form extends React.Component {
componentDidMount() {
this.taskCompleted = callbackWhenCompleted(this.submitForm);
}
imageUploaded = () => this.taskCompleted('imageUploaded');
submitButtonClicked = () => this.taskCompleted('submitButtonClicked');
submitForm = () => { /* */ }
render() { /* */ }
}
What are some better ways of solving this problem? Thanks!
You can store imageUploaded and submitButtonClicked in your Form component state instead and check if both are true after you change one of them and call submitForm if that's the case.
Example
class Form extends React.Component {
state = {
imageUploaded: false,
submitButtonClicked: false
};
imageUploaded = () => {
this.setState({ imageUploaded: true }, this.checkIfComplete);
};
submitButtonClicked = () => {
this.setState({ submitButtonClicked: true }, this.checkIfComplete);
};
checkIfComplete = () => {
const { imageUploaded, submitButtonClicked } = this.state;
if (imageUploaded && submitButtonClicked) {
this.submitForm();
}
};
submitForm = () => {
// ...
};
render() {
// ...
}
}
This is what I've been trying right now, but it keeps rendering the same page after the first switch.
I tried to follow the react lifecycle to figure it out, but it doesn't work as I intended.
I want it to start from RepoPage -> TimerPage -> ClockPage -> RepoPage and so on.
How can I fix it?
EDIT:
const REPO_PAGE = '5_SECONDS';
const COUNTDOWN_PAGE = '15_SECONDS';
const TIME_PAGE = '15_SECONDS';
const RepoComponent = () => (
<div>REPO</div>
);
const SprintCountComponent = () => (
<div>TIMER></div>
);
const DateTimeComponent = () => (
<div>CLOCK</div>
);
class App extends Component {
constructor(props) {
super(props);
this.state = {
repoTitles: [],
userData: [],
commitData: [],
recentTwoData: [],
currentPage: REPO_PAGE,
imgURL: ""
};
}
componentDidUpdate() {
const {currentPage} = this.state;
const isRepoPage = currentPage === REPO_PAGE;
const isTimePage = currentPage === TIME_PAGE;
if (isRepoPage) {
this._showDateTimePageDelayed();
} else if (isTimePage) {
this._showCountDownPageDelayed();
} else {
this._showRepoPageDelayed();
}
}
componentDidMount() {
this._showCountDownPageDelayed();
};
_showCountDownPageDelayed = () => setTimeout(() => {this.setState({currentPage: COUNTDOWN_PAGE})}, 5000);
_showRepoPageDelayed = () => setTimeout(() => {this.setState({currentPage: REPO_PAGE})}, 5000);
_showDateTimePageDelayed = () => setTimeout(() => {this.setState({currentPage: TIME_PAGE})}, 5000);
render() {
const {currentPage} = this.state;
const isRepoPage = currentPage === REPO_PAGE;
const isTimePage = currentPage === TIME_PAGE;
if(isRepoPage) {
return <RepoComponent/>;
} else if(isTimePage) {
return <DateTimeComponent/>;
} else {
return <SprintCountComponent/>;
}
}
}
You did not have return or else so this._showCountDownPageDelayed() is always executed.
if (isRepoPage) {
this._showDateTimePageDelayed();
} else if(isTimePage) {
this._showRepoPageDelayed();
} else {
this._showCountDownPageDelayed();
}
Using setInterval might give you a cleaner solution.
Edit: Your logic causes it to alternate between RepoPage and TimePage. A quick fix would be:
if (isRepoPage) {
this._showDateTimePageDelayed();
} else if (isTimePage) {
this._showCountDownPageDelayed();
} else {
this._showRepoPageDelayed();
}
I'm working on jest unit testing using react-test-renderer.The test cases fails and showing this error
"TypeError: this.props.myMaterials.fetch is not a function"
where this.props.notes.fetch is inside the componentWillMount.Is there any solution to fix this without using enzyme?
myComponent.jsx :
class myComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
column: this.getColumns(),
pageNotFound: false
};
}
componentWillMount() {
this.props.notes.fetch(this.props.courseId);
window.addEventListener('resize', this.handleResizeEvent);
this.handleError = EventBus.on(constants.NOTES_NOT_FOUND, () => {
this.setState({ pageNotFound: true });
});
}
componentDidMount() {
window.addEventListener('resize', this.handleResizeEvent);
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResizeEvent);
this.handleError();
}
handleResizeEvent = () => {
this.setState({ column: this.getColumns() });
};
getColumns = () => (window.innerWidth > (constants.NOTES_MAX_COLUMNS * constants.NOTES_WIDTH) ?
constants.NOTES_MAX_COLUMNS :
Math.floor(window.innerWidth / constants.NOTES_WIDTH))
callback = (msg, data) => {
}
render() {
const { notes, language } = this.props;
if (this.state.pageNotFound) {
return (<div className="emptyMessage"><span>Empty</span></div>);
}
if (notes.loading) {
return (<Progress/>);
}
// To Refresh Child component will receive props
const lists = [...notes.cards];
return (
<div className="notesContainer" >
<NoteBook notesList={lists} callback={this.callback} coloums={this.state.column} />
</div>
);
}
}
myComponent.propTypes = {
notes: PropTypes.object,
courseId: PropTypes.string,
language: PropTypes.shape(shapes.language)
};
export default withRouter(myComponent);
myComponent.test.jsx:
const tree = renderer.create(
<myComponent.WrappedComponent/>).toJSON();
expect(tree).toMatchSnapshot();
});
Its pretty evident from the error that while testing you are not supplying the prop notes which is being used in your componentWillMount function. Pass it when you are creating an instance for testing and it should work.
All you need to do is this
const notes = {
fetch: jest.fn()
}
const tree = renderer.create(
<myComponent.WrappedComponent notes={notes}/>).toJSON();
expect(tree).toMatchSnapshot();
});
One more thing that you should take care is that your component names must begin with Uppercase characters.
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
column: this.getColumns(),
pageNotFound: false
};
}
componentWillMount() {
this.props.notes.fetch(this.props.courseId);
window.addEventListener('resize', this.handleResizeEvent);
this.handleError = EventBus.on(constants.NOTES_NOT_FOUND, () => {
this.setState({ pageNotFound: true });
});
}
componentDidMount() {
window.addEventListener('resize', this.handleResizeEvent);
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResizeEvent);
this.handleError();
}
handleResizeEvent = () => {
this.setState({ column: this.getColumns() });
};
getColumns = () => (window.innerWidth > (constants.NOTES_MAX_COLUMNS * constants.NOTES_WIDTH) ?
constants.NOTES_MAX_COLUMNS :
Math.floor(window.innerWidth / constants.NOTES_WIDTH))
callback = (msg, data) => {
}
render() {
const { notes, language } = this.props;
if (this.state.pageNotFound) {
return (<div className="emptyMessage"><span>Empty</span></div>);
}
if (notes.loading) {
return (<Progress/>);
}
// To Refresh Child component will receive props
const lists = [...notes.cards];
return (
<div className="notesContainer" >
<NoteBook notesList={lists} callback={this.callback} coloums={this.state.column} />
</div>
);
}
}
MyComponent.propTypes = {
notes: PropTypes.object,
courseId: PropTypes.string,
language: PropTypes.shape(shapes.language)
};
export default withRouter(MyComponent);
Have you tried giving your component a stub notes.fetch function?
Let isFetched = false;
const fakeNotes = {
fetch: () => isFetched = true
}
That way you can test that fetch is called without making a request. I'm not sure, but the test runner is running in node, and I think you may need to require fetch in node, and so the real notes may be trying to use the browser's fetch that does not exist.
I'm not an expert, but I believe it is good practice to use a fakes for side effects/dependencies anyway, unless the test specifically is testing the side effect/dependency.
Pass notes as props to your component like <myComponent.WrappedComponent notes={<here>} /> and also put a check like this.props.notes && this.props.notes.fetch so that even if your props aren't passed you don't get an error.
Sorry I couldn't come up with a more specific title for this question. When I execute the below snippet I get the following warning:
Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the Typewriter component.
However, if the render() in MyComponent is changed to the following, I get no such warning:
render() {
return (
<div>
<h1>
<Typewriter />
{ this.state.render == 1 && "Render 1" }
{ this.state.render == 2 && "Render 2" }
{ this.state.render == 3 && "Render 3" }
</h1>
</div>
);
}
How do I properly unmount this rendered Typewriter component that itself is performing some mounting and unmounting actions? Thanks!
class Typewriter extends React.Component {
constructor(props) {
super();
this.state = {
finalText: ''
}
this.typeWriter = this.typeWriter.bind(this);
}
typeWriter(text, n) {
if (n < (text.length)) {
if (n + 1 == (text.length)) {
let j = text.substring(0, n+1);
this.setState({ finalText: j });
n++;
}
else {
let k = text.substring(0, n+1) + '|';
this.setState({ finalText: k });
n++;
}
setTimeout( () => { this.typeWriter(text, n) }, 100 );
}
}
componentDidMount() {
this.typeWriter('testing_typewriter', 0);
}
render() {
return (
<div>
{ this.state.finalText }
</div>
);
}
}
class MyComponent extends React.Component {
constructor(props) {
super();
this.state = {
render: 1,
update: false
};
this.interval = null;
}
componentDidMount() {
this.interval = setTimeout( () =>
this.rendering(), 1700
);
}
componentWillUpdate(nextProps, nextState) {
if (this.state.render < 3) {
this.interval = setTimeout( () =>
this.rendering(), 1200
);
}
}
componentWillUnmount() {
clearInterval(this.interval);
this.interval = null;
}
rendering() {
if (this.state.render < 3) {
if (this.interval) {
this.setState({ render: this.state.render + 1 });
}
}
}
render() {
return (
<div>
<h1>
{ this.state.render == 1 && "Render 1" }
{ this.state.render == 2 && <Typewriter /> }
{ this.state.render == 3 && "Render 3" }
</h1>
</div>
);
}
}
ReactDOM.render(<MyComponent />, 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>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="app"></div>
I had a similar issue and I solved it by clearing the Timeout/Interval in the componentWillUnmount function
in the typewriter function you need to keep track of this timeout:
setTimeout( () => { this.typeWriter(text, n) }, 100 );
with something like
this._timer = setTimeout( () => { this.typeWriter(text, n) }, 100 );
Then add a lifecycle method:
componentWillUnmount() {
window.clearTimeout(this._timer);
}