I'm helping another developer by adding pagination to a table view of search results. I am new to ReactJS, but am very familiar with vanilla JavaScript. I borrowed code from a ReactJS course, which works fine at home using only Node as the server. At work, we are using Node, Next and PM2.
I'm calling an event handler from an onClick event. The event handler, see below, calls this.setState, but when clicked, I get the error:
TypeError: this is undefined
I have tried an arrow function and a normal function. The normal function throws the "undefined" error and the arrow function throws the compile error: "eventHandler is not defined". I've searched here and done a Google search, and I can't find the solution to this problem. There is no class in this component, just some const
function handlePageChange(page){
this.setState((page)=>({currentPage: page}));
}
const Pagination = (props)=>{
const {itemsCount, pageSize}=props;
const pagesCount = Math.ceil(itemsCount / pageSize);
if (pagesCount===1) return null;
let active =1;
const pages = _.range(1,pagesCount+1);
return (
<nav>
<ul className="pagination">
{pages.map(page=>(
<li key={page} active={page===active}>
<a onClick={()=>handlePageChange(page)}>
{page}
</a>
</li>
))}
</ul>
</nav>
)
}
Const Demo = (props)=>{
return (
// ... div, container, table, then the pagination:
<Pagination
itemsCount={props.data.length}
pageSize={pageSize}
currentPage={page}
/>
)}
async function getInitialProps(){}
export default Demo
The pagination displays properly when search results are returned. For example, I get 1 and 2 if there are two pages of results. But when I click on either number, I get TypeError: this is undefined on the this.setState line.
The state must be a part of a function/class, it will be much easier reading the main concepts then googling all the way up to the solution.
class Pagination extends React.Component {
state = {
currentPage: 0
};
handlePageChange = page => {
this.setState({ currentPage: page });
};
render() {
const { itemsCount, pageSize } = this.props;
const pagesCount = Math.ceil(itemsCount / pageSize);
if (pagesCount === 1) return null;
let active = 1;
const pages = _.range(1, pagesCount + 1);
return (
<nav>
<ul className="pagination">
{pages.map(page => (
<li key={page} active={page === active}>
<a onClick={() => this.handlePageChange(page)}>{page}</a>
</li>
))}
</ul>
</nav>
);
}
}
Add useState hook since there is no this.setState in functional component
import React, { useState } from 'react';
const Pagination = (props) => {
const [currPage, setCurrPage] = useState(/* default*/);
...
{pages.map(page => (
<li key={page} active={page === currPage}>
<a onClick={() => setCurrPage(page)}>{page}</a>
</li>
))}
...
}
Related
I'm trying to send a delete request to delete an item from an API.
The API request is fine when clicking on the button. But Item get's deleted only after refreshing the browser!
I'm not too sure if I should add any parameter to SetHamsterDeleted for it to work?
This is what my code looks like.
import React, {useState} from "react";
const Hamster = (props) => {
const [hamsterDeleted, setHamsterDeleted] = useState("")
async function deleteHamster(id) {
const response = await fetch(`/hamsters/${id}`, { method: "DELETE" });
setHamsterDeleted()
}
return (
<div>
<p className={props.hamster ? "" : "hide"}>
{hamsterDeleted}
</p>
<button onClick={() => deleteHamster(props.hamster.id)}>Delete</button>
<h2>{props.hamster.name}</h2>
<p>Ålder:{props.hamster.age}</p>
<p>Favorit mat:{props.hamster.favFood}</p>
<p>Matcher:{props.hamster.games}</p>
<img src={'./img/' + props.hamster.imgName} alt="hamster"/>
</div>
)
};
export default Hamster;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Imagine you have a parent component (say HamstersList) that returns/renders list of these Hamster components - it would be preferable to declare that deleteHamster method in it, so it could either: a) pass some prop like hidden into every Hamster or b) refetch list of all Hamsters from the API after one got "deleted" c) remove "deleted" hamster from an array that was stored locally in that parent List component.
But since you are trying to archive this inside of Hamster itself, few changes might help you:
change state line to const [hamsterDeleted, setHamsterDeleted] = useState(false)
call setHamsterDeleted(true) inside of deleteHamster method after awaited fetch.
a small tweak of "conditional rendering" inside of return, to actually render nothing when current Hamster has hamsterDeleted set to true:
return hamsterDeleted ? null : (<div>*all your hamster's content here*</div>)
What do you want to do in the case the hamster is deleted? If you don't want to return anything, you can just return null.
I'm not too sure if I should add any parameter to SetHamsterDeleted for it to work?
Yes, I'd make this a boolean instead. Here's an example:
import React, { useState } from "react";
const Hamster = (props) => {
const [hamsterDeleted, setHamsterDeleted] = useState(false);
async function deleteHamster(id) {
const response = await fetch(`/hamsters/${id}`, { method: "DELETE" });
setHamsterDeleted(true);
}
if (hamsterDeleted) return null;
return (
<div>
<p className={props.hamster ? "" : "hide"}>
{hamsterDeleted}
</p>
<button onClick={() => deleteHamster(props.hamster.id)}>Delete</button>
<h2>{props.hamster.name}</h2>
<p>Ålder:{props.hamster.age}</p>
<p>Favorit mat:{props.hamster.favFood}</p>
<p>Matcher:{props.hamster.games}</p>
<img src={'./img/' + props.hamster.imgName} alt="hamster"/>
</div>
);
};
HOWEVER! Having each individual hamster keep track of its deleted state doesn't sound right (of course I don't know all your requirements but it seems odd). I'm guessing that you've got a parent component which is fetching all the hamsters - that would be a better place to keep track of what has been deleted, and what hasn't. That way, if the hamster is deleted, you could just not render that hamster. Something more like this:
const Hamsters = () => {
const [hamsers, setHamsters] = useState([]);
// Load the hamsters when the component loads
useEffect(() => {
const loadHamsters = async () => {
const { data } = await fetch(`/hamsters`, { method: "GET" });
setHamsters(data);
}
loadHamsters();
}, []);
// Shared handler to delete a hamster
const handleDelete = async (id) => {
await fetch(`/hamsters/${id}`, { method: "DELETE" });
setHamsters(prev => prev.filter(h => h.id !== id));
}
return (
<>
{hamsters.map(hamster => (
<Hamster
key={hamster.id}
hamster={hamster}
onDelete={handleDelete}
/>
))}
</>
);
}
Now you can just make the Hamster component a presentational component that only cares about rendering a hamster, eg:
const Hamster = ({ hamster, onDelete }) => {
const handleDelete = () => onDelete(hamster.id);
return (
<div>
<button onClick={handleDelete}>Delete</button>
<h2>{hamster.name}</h2>
<p>Ålder:{hamster.age}</p>
<p>Favorit mat:{hamster.favFood}</p>
<p>Matcher:{hamster.games}</p>
<img src={'./img/' + hamster.imgName} alt="hamster"/>
</div>
);
};
On my site, the <ArticleList> is supposed to update when one navigates between columns. This works when you go from the home page to a column, or from an article to a column. But if you go from column to column, it doesn't work. The page doesn't update at all, but the url changes. The links to each column stay the same, as they are part of the <Layout> component, which every page has.
Edit
I figured out now that I can just use <a> and omit <Link> entirely, but this would slow down the page navigation.
Edit 2
This is part of my <Layout> component where I render the links to the columns:
<nav className={layout.columnContainer}>
{columns.map(({ id, name }) =>
this.props.currentColumn ? (
<a key={id} href={`/columns/${name}`}>
{name}
</a>
) : (
<Link key={id} href="/columns/[name]" as={`/columns/${name}`}>
<a>{name}</a>
</Link>
),
)}
</nav>
Edit 3
My minimal reproducible example is on GitHub, and I get the same unexpected results.
Edit 4
I found that the reason it wasn't working was I implemented a search bar that put the children prop in a state and modified this.
Constructor:
constructor(props) {
super(props);
this.searchArticlesKeyType = this.searchArticlesKeyType.bind(this);
this.state = {displayedMain: props.children};
}
Inside the render method are the column links (nav) and the problematic search input element.
<nav className={layout.columnContainer}>
{
columns.map(({id, name}) => (
<Link key={id} href="/columns/[name]" as={`/columns/${name}`}><a>{name}</a></Link>
))
}
</nav>
<div className={layout.search}>
<input type="search" name="q" onKeyUp={this.searchArticlesKeyType} />
</div>
async searchArticlesKeyType(e) {
// Some code
this.setState({
displayedMain: <ArticleList articles={JSON.stringify(filteredArticles)}/>
// More code
});
}
I think your main issue here is the way you're implementing the search feature, you don't want to store components in state instead you need to pass the search text to the articlelist component and do the filtering there.
There are several ways to implement communication between 2 unrelated components, it could be via context, redux, or even make a portal in the layout to render the seach input from the column component, but in this case I think the best option is to store the search text in the url:
First make the input event update the url using next/router, your layout will look like this:
import { useRouter } from 'next/router'
...
function Layout(props) {
const {columns} = props;
const { push, asPath, query } = useRouter()
const searchArticlesKeyType = (e) => {
const q = e.target.value;
const [url] = asPath.split('?');
push(`${url}?q=${q}`, undefined, { shallow: true });
}
return (
<div>
...
<div>
<input type="search" name="q" defaultValue={query.q} onKeyUp={searchArticlesKeyType} />
</div>
...
</div>
)
}
And then you do the filtering in articlelist component
import Link from "next/link";
import { useRouter } from 'next/router'
export default function ArticleList(props) {
const { query } = useRouter();
const q = query.q || "";
const filteredArticles = props.articles.filter(
(item) => item.title.includes(q) || item.body.includes(q)
);
return (
<ul className="grid">
{filteredArticles.map((item) => (
<div key={item.id}>
<Link
key={item.id}
href="/articles/[title]"
as={`/articles/${item.title}`}
>
<a>
<p>
<strong>{item.title}</strong>
</p>
<p>{item.body.substring(0, 100)}</p>
</a>
</Link>
</div>
))}
</ul>
);
}
TL;DR I am making a reusable Button function component. My useState() hook for the button label is updating every Button instance. How can I prevent this?
I am very new to React and building a Book Finder app in order to learn. So far my app has a BookList and a ReadingList. Each BookDetail in either list has a Button to add/remove that book from the ReadingList. The add/remove function works (phew), but using useState to update the Button's label updates every instance of the Button component, and not just the one that was clicked.
Buttons on books in the BookList start with label 'Add to Reading List', but if I click any of them, all of them update to 'Remove from Reading List'.
I've tried moving the logic around into the Button component or either List component but I just end up breaking the function.
App.js
function App() {
const books = useState([])
const [booksToRead, setBooksToRead] = useState([])
const [addRemove, setAddRemove] = useState(true)
const [label, setLabel] = useState('Add to Reading List')
function handleAddBook(book) {
const newID = book.title_id
if( (typeof booksToRead.find(x => x.title_id === newID)) == 'undefined' ) {
setBooksToRead([...booksToRead, book])
}
}
function handleRemoveBook(book) {
console.log(book)
const array = booksToRead
const index = array.indexOf(book)
const newArray = [...array.slice(0, index), ...array.slice(index +1)]
setBooksToRead(newArray)
}
function addOrRemove(book) {
if( addRemove ) {
handleAddBook(book)
setLabel('Remove from Reading List')
setAddRemove(false)
} else {
handleRemoveBook(book)
setLabel('Add to Reading List')
setAddRemove(true)
}
}
return (
<main>
<BookList books={books} handleAddBook={handleAddBook} addOrRemove={addOrRemove} label={label} />
<ReadingList booksToRead={booksToRead} handleRemoveBook={handleRemoveBook} />
</main>
);
}
export default App;
BookList.js
function BookList ({ book, label, handleAddBook, addOrRemove }) {
return (
<div className="booklist">
{BookData.map((book, index) => {
const onAddBook = () => addOrRemove(book)
return (
<div key={index} className="card">
<BookDetail key={book.title_id} book={book} />
<Button key={index + 'btn'} label={label} onClick={onAddBook} />
</div>
)
})}
</div>
)
}
export default BookList
And finally, Button.js
export default function Button({ styleClass, label, onClick }) {
return (
<button className='btn' onClick={(event) => onClick(event)}>
{label}
</button>
)
}
Unstyled example in codesandbox: https://codesandbox.io/s/cool-rgb-fksrp
Can you make these changes and let me know if there any progress:
<Button label={label} onClick={() => addOrRemove(book)} />
<button className='btn' onClick={onClick}>
It looks like that in button you are passing event instead of book as function parameter
As it is right now, you are declaring a single label and using that same one on all your book entries. This is why they all display the same label. You would need to keep track of the label of each book, for instance by keeping the label as a field in the book object.
For example:
const books = useState([{ label: 'Add to reading list', addRemove: true }])
And then:
function addOrRemove(book) {
if( book.addRemove ) {
handleAddBook(book)
book.label = 'Remove from Reading List'
book.addOrRemove = false
} else {
handleRemoveBook(book)
book.label = 'Add to Reading List'
book.addOrRemove = true
}
}
This way, each book has it's own label.
I have a link to return home and within that there is a button to remove an item from an array. To prevent the link to redirect to the home screen and to just remove the item from the array I am need to use ev.preventDefault().
Is it possible to pass an ev to a react method without using an arrow function in the render method? From my research and specifically the answer here it appears that the following is the only way to do so.
I am concerned that the arrow function causes a re-render every time, since new function is created on each render.
removeItem(label, e) {
e.preventDefault();
this.props.removeItem(label.id);
}
render () {
const { label } = this.props;
return (
<Link to'/'>
<span>Go Home</span>
<span onClick={(e) => this.removeItem(label, e) }> Click me <span>
</Link>
);
}
You could just remove the label parameter and get label from this.props directly. Refactor your app like this:
removeItem(ev) {
ev.preventDefault();
this.props.removeItem(this.props.label.id);
}
render () {
const { label } = this.props;
return (
<Link to'/'>
<span>Go Home</span>
<span onClick={this.removeItem}> Click me <span>
</Link>
);
}
This way, React will automatically pass the click event to your removeItem method.
Although, it should be said, that creating a new function on every render, probably isn't all that expensive.
In the light of avoiding to re-create the function you could extract the parameters into the class props. Here are some examples
For example this will create a new function all the time.
var List = React.createClass({
render() {
return (
<ul>
{this.props.items.map(item =>
<li key={item.id} onClick={this.props.onItemClick.bind(null, item.id)}>
...
</li>
)}
</ul>
);
}
});
Now it will not re-create the functions on re-rendering.
var List = React.createClass({
render() {
return (
<ul>
{this.props.items.map(item =>
<ListItem key={item.id} item={item} onItemClick={this.props.onItemClick} />
)}
</ul>
);
}
});
var ListItem = React.createClass({
render() {
return (
<li onClick={this._onClick}>
...
</li>
);
},
_onClick() {
this.props.onItemClick(this.props.item.id);
}
});
Here is some SO explanation React js onClick can't pass value to method
Looking at the docs regarding functional vs classical components, it seems you only need classical components if you want to create an instance that gives you access to this or you want the lifecycle methods. Does that mean a functional component can only have a render inside? If it needs to handle a click, it has to go through a onClick listener on the markup that links directly to its props and there's no way to go through a handleClick function?
const Type = ({onTypeClick, name}) => {
<li
onClick={onTypeClick.bind(null, name)}
>
{name}
</li>
}
VS
const Type = React.createClass({
handleClick (e) {
e.preventDefault()
this.props.onTypeClick(this.props.name)
},
render() {
return (
<li onClick={handleClick}>
{this.props.name}
</li>
)
}
})
A stateless component returns the result of render function, however it's just a function so it has access to stuff in closure.
const handleClick = (e, props) => {
e.preventDefault()
props.onTypeClick(props.name)
}
const Type = props => {
// don't forget to return!
return (
<li onClick={event => handleClick(event, props)}>
{props.name}
</li>
)
}