I am trying to do auto text animation in my React project. I can make it work in VanillaJS but I don't know how can I do it in React. (I am beginner at React.)
import React, { Component } from 'react'
class AutoTextSection extends Component {
writeText = () => {
let idx = 1
const text = "This is the text sentence."
document.getElementById('auto-text').textContent = text.slice(0, idx)
idx++
if (idx > document.getElementById('auto-text').length) {
idx = 1
}
setTimeout(this.writeText, 1000)
}
render() {
return (
<section id="auto-text-sec">
<h2 className="text-light" id="auto-text">
{this.writeText()}
</h2>
</section>
)
}
}
Just I can see the first letter. Then it throws me this error :
TypeError: Cannot set property 'textContent' of null.
In react, you must use ref to be able to access the DOM element directly.
Change your code:
import React, { Component } from 'react'
class AutoTextSection extends Component {
constructor(props) {
super(props);
this.autoTextRef = React.createRef();
}
writeText = () => {
let idx = 1
const text = "This is the text sentence."
this.autoTextRef.current.textContent = text.slice(0, idx)
idx++
if (idx > this.autoTextRef.current.length) {
idx = 1
}
setTimeout(this.writeText, 1000)
}
render() {
return (
<section id="auto-text-sec">
<h2 className="text-light" ref={this.autoTextRef}>
{this.writeText()}
</h2>
</section>
)
}
}
In this line, most likely you wanted to use a text node, which is located inside the DOM element:
if (idx > this.autoTextRef.current.length) {
So use:
if (idx > this.autoTextRef.current.textContent.length) {
But your code still contains bugs. It is better to start typing in the componentDidMount lifecycle hook.
Another obvious problem is that when you call writeText, you will always have idx = 1; Therefore, this state must be endured higher. You can use state for this.
Also in the code there is no condition to terminate the recursive call.
The final minimal working code should look like this:
import React, { Component } from 'react'
class AutoTextSection extends Component {
constructor(props) {
super(props);
this.autoTextRef = React.createRef();
this.state = {
idx: 1,
}
}
componentDidMount() {
this.writeText()
}
writeText = () => {
console.log('writeText')
const text = "This is the text sentence."
this.autoTextRef.current.textContent = text.slice(0, this.state.idx)
this.setState((prev) => ({
idx: ++prev.idx,
}))
if (this.state.idx <= text.length) {
console.log('writeText recursion')
setTimeout(this.writeText, 100)
}
}
render() {
return (
<section id="auto-text-sec">
<h2 className="text-light" ref={this.autoTextRef} />
</section>
)
}
}
Related
I have made for me a Tutorial-Project where I collect various React-Examples from easy to difficult. There is a "switch/case" conditional rendering in App.js, where I - depending on the ListBox ItemIndex - load and execute the selected Component.
I am trying to optimize my React code by removing the "switch/case" function and replacing it with a two dimensional array, where the 1st column contains the Component-Name 2nd column the Object. Further I would like to lazy-load the selected components.
Everything seems to work fine, I can also catch the mouse events and also the re-rendering begins but the screen becomes white... no component rendering.
App.js
import SampleList, { sampleArray } from './SampleList';
class App extends React.Component {
constructor(props) {
super(props);
this.selectedIndex = -1;
}
renderSample(index) {
if((index >= 0) && (index < sampleArray.length)) {
return React.createElement(sampleArray[index][1])
} else {
return <h3>Select a Sample</h3>;
}
}
render() {
return (
<header>
<h1>React Tutorial</h1>
<SampleList myClickEvent={ this.ClickEvent.bind(this) }/>
<p />
<div>
<Suspense> /**** HERE WAS MY ISSUE ****/
{ this.renderSample(this.selectedIndex) }
</Suspense>
</div>
</header>
);
}
ClickEvent(index) {
this.selectedIndex = index;
this.forceUpdate();
}
}
SampleList.js
import React from 'react';
const SimpleComponent = React.lazy(() => import('./lessons/SimpleComponent'));
const IntervalTimerFunction = React.lazy(() => import('./lessons/IntervalTimerFunction'));
const sampleArray = [
["Simple Component", SimpleComponent],
["Interval Timer Function", IntervalTimerFunction]
];
class SampleList extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
this.selectOptions = sampleArray.map((Sample, Index) =>
<option>{ Sample[0] }</option>
);
}
render() {
return (
<select ref={this.myRef} Size="8" onClick={this.selectEvent.bind(this)}>
{ this.selectOptions }
</select>
);
}
selectEvent() {
this.props.myClickEvent(this.myRef.current.selectedIndex);
}
}
export default SampleList;
export { sampleArray };
You have several issues in that code:
If you use React.lazy to import components dynamically, use Suspense to show a fallback;
The select can listen to the change event, and receive the value of the selected option, that is convenient to pass the index in your case;
Changing a ref with a new index doesn't trigger a re-render of your components tree, you need to perform a setState with the selected index;
I suggest you to switch to hooks, to have some code optimizations;
Code:
import React, { Suspense, useState, useMemo } from 'react';
const SimpleComponent = React.lazy(() => import('./lessons/SimpleComponent'));
const IntervalTimerFunction = React.lazy(() =>
import('./lessons/IntervalTimerFunction'));
const sampleArray = [
['Simple Component', SimpleComponent],
['Interval Timer Function', IntervalTimerFunction],
];
export default function App() {
const [idx, setIdx] = useState(0);
const SelectedSample = useMemo(() => sampleArray[idx][1], [idx]);
const handleSelect = (idx) => setIdx(idx);
return (
<Suspense fallback={() => <>Loading...</>}>
<SampleList handleSelect={handleSelect} />
<SelectedSample />
</Suspense>
);
}
class SampleList extends React.Component {
constructor(props) {
super(props);
}
selectEvent(e) {
this.props.handleSelect(e.target.value);
}
render() {
return (
<select ref={this.myRef} Size="8" onChange={this.selectEvent.bind(this)}>
{sampleArray.map((sample, idx) => (
<option value={idx}>{sample[0]}</option>
))}
</select>
);
}
}
Working example HERE
I'm building a pagination component and I'm struggling to execute a for loop so I can dynamically generate the pages. I initially had a function component, but I want to switch it to a class component so I can manage state in it. (I know, I can use hooks, but Im practicing class components at the moment).
I initially added the for loop in the render method but it is executing the loop twice because the component ir rendering twice. Then, I tried componentDidMount() but it doesn't do anything... then used componentWillMount() and it worked. However, I know this could be bad practice.
Any ideas? See below the component with componentDidMount()
import React, { Component } from 'react';
import styles from './Pagination.module.css';
class Pagination extends Component {
state = {
pageNumbers: [],
selected: '',
};
componentDidMount() {
for (
let i = 1;
i <= Math.ceil(this.props.totalDogs / this.props.dogsPerPage);
i++
) {
this.state.pageNumbers.push(i);
}
}
classActiveForPagineHandler = (number) => {
this.setState({ selected: number });
};
render() {
return (
<div className={styles.PaginationContainer}>
<nav>
<ul className={styles.PageListHolder}>
{this.state.pageNumbers.map((num) => (
<li key={num}>
<a
href="!#"
className={
this.state.selected === num
? styles.Active
: styles.PageActive
}
onClick={() => {
this.props.paginate(num);
// this.props.classActiveForPagineHandler(num);
}}
>
{num}
</a>
</li>
))}
</ul>
</nav>
</div>
);
}
}
export default Pagination;
You better push all the numbers into array and then update pageNumbers state. this.state.pageNumbers.push(i); does not update state directly, you need use setState after your calculation completes.
componentDidMount() {
const { pageNumbers = [] } = this.state
const { totalDogs, dogsPerPage } = this.props
for (let i = 1; i <= Math.ceil(totalDogs / dogsPerPage); i++) {
pageNumbers.push(i);
}
this.setState({ pageNumbers })
}
Demo link here
you should not update state like this :
this.state.pageNumbers.push(i);
do this:
this.setState((s) => {
return {
...s,
pageNumbers: [...s.pageNumbers, i]
}
})
Do not mutate state directly in react component. Use setState for all updates.
componentDidMount() {
const pageNumbers = [];
for (
let i = 1;
i <= Math.ceil(this.props.totalDogs / this.props.dogsPerPage);
i++
) {
pageNumbers.push(i);
}
this.setState({ pageNumbers });
}
Alternatively, you can simplify the code using Array.from for this case.
componentDidMount() {
this.setState({
pageNumbers: Array.from(
{ length: Math.ceil(this.props.totalDogs / this.props.dogsPerPage) },
(_, i) => i + 1
),
});
}
I am working on trying to get this counter for pintsLeft to work. This is my first project with React and I feel that I am either not passing the property of the array correctly or my function code is not set correctly.
^^^^KegDetail.js^^^^
import React from "react";
import PropTypes from "prop-types";
function KegDetail(props){
const { keg, onClickingDelete} = props
return (
<React.Fragment>
<hr/>
<h2>{keg.name} Made By {keg.brewery}</h2>
<p>abv {keg.abv}</p>
<h3>price {keg.price}</h3>
<p>{keg.pintsLeft} total pints left</p> {/* Make this a percentage */}
<hr/>
<button onClick={ props.onClickingEdit }>Update Keg</button>
<button onClick={()=> onClickingDelete(keg.id) }>Delete Keg</button>
<button onClick={()=> this.onSellingPint()}>Sell A Pint!</button>
</React.Fragment>
);
}
KegDetail.propTypes = {
keg: PropTypes.object,
onClickingDelete: PropTypes.func,
onClickingEdit:PropTypes.func,
onSellingPint:PropTypes.func
}
export default KegDetail;
That was my KegDetail.js
import React, {useState} from "react";
import NewKegForm from "./NewKegForm";
import DraftList from "./DraftList";
import KegDetail from "./KegDetail";
import EditKegForm from "./EditKegForm";
class DraftControl extends React.Component {
constructor(props){
super(props);
this.state = {
kegFormVisibleOnPage: false,
fullDraftList: [],
selectedKeg: null,
editing: false,
pints: 127,
};
this.handleClick = this.handleClick.bind(this);
this.handleSellingPint = this.handleSellingPint.bind(this);
}
handleClick = () => {
if (this.state.selectedKeg != null){
this.setState({
kegFormVisibleOnPage: false,
selectedKeg: null,
editing: false
});
} else {
this.setState(prevState => ({
kegFormVisibleOnPage: !prevState.kegFormVisibleOnPage,
}));
}
}
handleSellingPint = () => {
this.setState({
pints:this.state.pints-1
})
};
render() {
let currentlyVisibleState = null;
let buttonText = null;
if (this.state.editing){
currentlyVisibleState = <EditKegForm keg = {this.state.selectedKeg} onEditKeg = {this.handleEditingKegInDraftList} />
buttonText = "Return to the Draft List"
}
else if (this.state.selectedKeg != null){
currentlyVisibleState = <KegDetail keg = {this.state.selectedKeg} onClickingDelete = {this.handleDeletingKeg}
onClickingEdit = {this.handleEditClick} onSellingPint = {this.handleSellingPint}/>
buttonText = "Return to the Keg List"
My DraftControl.js code
I don't know what I am doing wrong. I cant get the keg.pintsLeft to pass a number when I console.log, So I may be targeting it incorrectly.
Thanks again!
Try it like this:
handleSellingPint = () => {
this.setState(prevState => {
return {
pints: prevState.pints-1
}
})
};
edit
Also, you invoke the onSellingPint() in a wrong way.
It's not a class component, so React doesn't know what does this refer to.
The function itself is passed in as a prop, so you should reference it like this: <button onClick={() => props.onSellingPint() />
handleSellingPint = (id) => {
const clonedArray = [...this.state.fullDraftList]
for (let i = 0; i < this.state.fullDraftList.length; i++){
if (clonedArray[i].id === id){
clonedArray[i].pintsLeft -= 1
}
}
this.setState({
fullDraftList: clone
});
}
Is what I came up with.
Since you are alteriting a state within an array, you need to clone the array and work on that array, not the "real" one.
Thanks for all your help!
I am trying to create a typewriter animation like this in my es6 component (essentially, iteratively renders additional passed elements or letters). However, any time I execute / render this component, all that is rendered is the first element / letter, 'a', of the larger set, 'abc'. The timeout period is working fine, so I think that the for loop is failing. How do I properly run a for loop over a setTimeout function in es6 such that my new elements will render? Thanks.
import React from 'react';
import { CSSTransitionGroup } from 'react-transition-group';
import Radium from 'radium';
export default class Logo extends React.Component {
constructor(props) {
super();
this.state = {
final: ''
}
this.typeWriter = this.typeWriter.bind(this);
}
typeWriter(text, n) {
if (n < (text.length)) {
let k = text.substring(0, n+1);
this.setState({ final: k });
n++;
setTimeout( () => { this.typeWriter(text, n) }, 1000 );
}
}
render() {
this.typeWriter('abc', 0);
return (
<div>
<h1>{this.state.final}</h1>
</div>
);
}
}
module.exports = Radium(Logo);
Since this.typeWriter('abc', 0); is in the render function, whenever the state changes, it runs the typewriter method, which updates the state back to a.
Move the this.typeWriter('abc', 0); to componentDidMount(). It will start the type writer when the component has finished rendering.
class Logo extends React.Component {
constructor(props) {
super();
this.state = {
final: ''
}
this.typeWriter = this.typeWriter.bind(this);
}
typeWriter(text, n) {
if (n < (text.length)) {
let k = text.substring(0, n+1);
this.setState({ final: k });
n++;
setTimeout( () => { this.typeWriter(text, n) }, 1000 );
}
}
componentDidMount() {
this.typeWriter('abc', 0);
}
render() {
return (
<div>
<h1>{this.state.final}</h1>
</div>
);
}
}
ReactDOM.render(
<Logo />,
demo
);
<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>
<div id="demo"></div>
I'm implementing search with pagination in React. So far I found few examples of it, but all they use code with double setState(), before and after AJAX call to backend. For example my current solution is:
import React from "react"
import PropTypes from "prop-types"
import SearchField from "components/SearchField"
import SearchResults from "components/SearchResults"
import Item from "models/Item"
class Search extends React.Component {
constructor() {
super()
this.state = {
query: "",
page: 1,
foundItems: []
}
this.handleSearch = this.handleSearch.bind(this)
this.handlePageChange = this.handlePageChange.bind(this)
}
updateSearchResults() {
const query = this.state.query
const params = {
page: this.state.page
}
Item.search(query, params).then((foundItems) => {
this.setState({ foundItems })
})
}
handleSearch(event) {
this.setState({
query: event.target.value
}, this.updateSearchResults)
}
handlePageChange(data) {
this.setState({
page: data.selected + 1
}, this.updateSearchResults)
}
render() {
return (
<div className="search">
<SearchField onSearch={this.handleSearch} />
<SearchResults
onPageChange={this.handlePageChange}
onSelect={this.props.onSelect}
items={this.state.foundItems}
/>
</div>
)
}
}
Search.propTypes = {
onSelect: PropTypes.func.isRequired
}
export default Search
I know that I can change interface of updateSearchResults to receive query and page as arguments and then I can avoid first setState to pass values there, but it doesn't look like a good solution, because when list of search parameters will grow (sorting order, page size, filters for example) then it'll get a bit clumsy. Plus I don't like idea of manual state pre-management in handleSearch and handlePageChange functions in this way. I'm looking for a better implementation.
I am not fully sure what you are asking, but you can optimise your code a bit by doing the following:
class Search extends React.Component {
constructor() {
super()
this.page = 1;
this.query = "";
this.state = {
foundItems: []
}
this.handlePageChange = this.handlePageChange.bind(this)
}
updateSearchResults(event) {
if(typeof event === "object")
this.query = event.target.value;
const params = {
page: this.page
}
Item.search(this.query, params).then((foundItems) => {
this.setState({ foundItems })
})
}
handlePageChange(data) {
this.page = data.selected + 1;
this.updateSearchResults();
}
render() {
return (
<div className="search">
<SearchField onSearch={this.updateSearchResults} />
<SearchResults
onPageChange={this.handlePageChange}
onSelect={this.props.onSelect}
items={this.state.foundItems}
/>
</div>
)
}
}