I've created a very simple list as an exercise and now I'm trying to perform some CRUD operations over it.
Problem is that when I delete an item, it appears like the container is fetching the data BEFORE actually deleting the item.
I'm using a Rails API backed, and from the console I can clearly see that the two fetch calls are overlapping; first the items are loaded and then the item is deleted.
I tried wrapping the last line of the deleteItem function from this:
; this.fetchItems();
to this:
.then(this.fetchItems());
but still the problem occurs, and I have no clue why.
Code simplified for clarity:
Items.jsx (collection container)
import Item from './Item'
class Items extends Component {
state = { items: [] }
fetchItems = () => {
fetch('/api/v1/items')
.then(response => response.json())
.then(responseJson => {
this.setState({
items: responseJson
});
});
}
componentDidMount() {
this.fetchItems();
}
deleteItem = (id) => {
fetch('/api/v1/items/' + id, {
method: 'DELETE',
headers: { 'Content-Type' : 'application/json' }
})
.then(this.fetchItems());
}
render() {
var items = this.state.items.map((item, idx) => {
return <Item
key={item.id}
itemName={item.name}
itemId={item.id}
onDelete={this.deleteItem} />
});
return (
<div>
{items}
</div>
);
}
}
export default Items;
Item.jsx (actual item)
class Item extends Component {
state = { id: this.props.itemId, name: this.props.itemName }
render() {
return (
<div onClick={() => this.props.onDelete(this.state.id)}>
{this.state.name}
</div>
);
}
}
export default Item;
I think you are on the right track by adding the then, however I think that is where the immediate re load is happening.
Try adding a function call within the then as below
.then(() => this.fetchItems());
Sorry for the many "I thinks" but am not near a computer at the moment I can test on.
Related
I have a large list of items that can be filtered, updated, and deleted. I'm using ReactQuery to fetch the list of items like this
export function useLibraryItems() {
return useQuery(['items'], () => API.FETCH_LIBRARY_ITEMS().then(response => response.data))
}
// And used later in my component like this
const items = useLibraryItems()
Due to the size of the list I'm rendering the items through a virtualized list like this
const ItemRow = ({ index, style }) => (
<Item
key={index}
item={items.data[index]}
style={style}
/>
)
<FixedSizeList
height={virtualListDimensions.height}
width={virtualListDimensions.width}
itemSize={30}
itemCount={items.data.length}
>
{ItemRow}
</FixedSizeList>
A simplified version of my item component looks like this
function Item({ item, style }) {
const [_item, setItem] = useState({ ...item })
const updateItem = useItemUpdate()
const onSaveClick = () => {
updateItem.mutate({ ..._item })
}
return (
<div>
...inputs to update item values
<button>
Update
</button>
</div>
)
}
The update item mutation looks like this
export function useUpdateLibraryItem() {
let client = useQueryClient()
return useMutation(args => API.UPDATE_LIBRARY_ITEM(args.id, args.params).then(response => response.data), {
onMutate: async (args) => {
await client.cancelQueries(['items'])
let prev = client.getQueriesData(['items'])
client.setQueriesData('items', items => [
...items.map(item => {
if(item._id === args.id)
return { ...item, ...args.params }
else
return item
})
])
return { prev }
}
})
}
This is all working as expecting. Now I'm trying to optimize this by making each item a selector. One issue with this current implementation is that if there are updates to multiple items in the list and you save the changes for one item it will optimistically update that item and the list which will clear all other updates in the list that have not been saved. So my attempts to subscribe to a single item of the items list currently looks like this
const ItemRow = ({ index, style }) => (
<Item
key={index}
id={items.data[index]._id}
style={style}
/>
)
And a new useQuery hook to select the item from the cache
export const useLibraryItem = (id) => {
return useQuery(['items'], () => API.FETCH_LIBRARY_ITEMS().then(response => response.data), { select: (items) => items.find(item => item._id === id) })
}
function Item({ id, style }) {
const item = useLibraryItem(id)
const [_item, setItem] = useState({ ...initial_item })
useEffect(() => {
setItem({ ...item.data })
}, [item.status])
const updateItem = useItemUpdate()
const onSaveClick = () => {
updateItem.mutate({ ..._item })
}
return (
<div>
...inputs to update item values
<button>
Update
</button>
</div>
)
}
When this code compiles it runs infinitely. If i add a console.log into the useEffect I will see it endlessly. So to solve this I needed to add the id's into the query key which looks like this
export const useLibraryItem = (id) => {
return useQuery(['items', id], () => API.FETCH_LIBRARY_ITEMS().then(response => response.data), { select: (items) => items.find(item => item._id === id) })
}
return useMutation(args => API.UPDATE_LIBRARY_ITEM(args.id, args.params).then(response => response.data), {
onMutate: async (args) => {
await client.cancelQueries(['items', args.id])
let prev = client.getQueriesData(['items', args.id])
client.setQueriesData(['item', args.id], item => {
return { ...item, ...args.params }
})
return { prev }
}
})
}
When this code compiles it no longer runs infinitely, but it makes a new request to API.FETCH_LIBRARY_ITEMS for every item in the list. This must be due to no longer referencing the query key of ['items'] as its now ['items', id] so it no longer caches.
Does React Query not support selectors in the way that I'm trying to use them or is there a way to implement them the way that I am trying to?
*I'm working on a sandbox for this question and will be updating with the link shortly
Does React Query not support selectors in the way that I'm trying to use them or is there a way to implement them the way that I am trying to?
react-query has selectors, but you are not using them in your example:
export function useLibraryItems(select) {
return useQuery(['items'], () => API.FETCH_LIBRARY_ITEMS().then(response => response.data), { select })
}
export function useLibraryItem(id) {
return useLibraryItems(data => data.find(item => item.id === id))
}
every time you call useLibraryItem, you will use the libraryItems query and only select a slice of the resulting data (the item that matches your id). Keep in mind that with the default settings of react-query, you will still get a background refetch of the list query every time a new item query mounts, because refetchOnMount is true and staleTime is zero.
The best way is to set a staleTime on the list query that tells react-query for how long the data is considered fresh. During that time, there won't be any additional requests.
I am trying to create a simple table using ReactJS to display user information. Here's how the general code structure looks like:
class ParentComponent extends React.Component {
state = {
data : []
}
componentDidMount() {
// initializes state with data from db
axios.get("link/").then(res => {
this.setState({data: res.data});
});
// I should be able to call this.getData() instead
// of rewriting the axios.get() function but if I do so,
// my data will not show up
}
// retrieves array of data from db
getData = () => {
axios.get("link/").then(res => {
this.setState({data: res.data});
});
}
render() {
return (
<div>
<ChildComponent data={this.state.data} refetch={this.getData} />
</div>
)
}
}
Each of the generated rows should have a delete function, where I'll delete the entry from the database based on a given id. After the deletion, I want to retrieve the latest data from the parent component to be redisplayed again.
class ChildComponent extends React.Component {
// deletes the specified entry from database
deleteData = (id) => {
axios.get("deleteLink/" + id).then(res => {
console.log(res);
// calls function from parent component to
// re-fetch the latest data from db
this.props.refetch();
}).catch(err => {console.log(err)});
}
render() {
let rows = null;
if(this.props.data.length) {
// map the array into individual rows
rows = this.props.data.map(x => {
return (
<tr>
<td>{x.id}</td>
<td>{x.name}</td>
<td>
<button onClick={() => {
this.deleteData(x.id)
}}>
Delete
</button>
</td>
</tr>
)
})
}
return (
<div>
<table>
<thead></thead>
<tbody>
{rows}
</tbody>
</table>
</div>
)
}
}
The two problems which I encountered here are:
Logically, I should be able to call this.getData() from within componentDidMount(), but if I do so, the table doesn't load.
Whenever I try to delete a row, the table will not reflect the update even though the entry is removed from the database. The table will only be updated when I refresh the page or delete another row. Problem is, the component is always lagging behind by 1 update.
So far I have tried:
this.forceUpdate() - doesn't work
this.setState({}) - empty setState doesn't work either
changing componentDidMount() to componentDidUpdate() - error showing that I have "reached maximum depth" or something along that line
adding async await in front of axios - doesn't work
Any help is appreciated. Thanks in advance.
EDIT: I did some debugging and tracked down the issue, which is not relevant to my question. My deleteData() which is located in ChildComponent uses axios.post() instead of axios.get(), which I overlooked.
deleteData = (id) => {
axios.post("deleteLink/", id).then(res => {
console.log(res);
// calls function from parent component to
// re-fetch the latest data from db
this.props.refetch();
}).catch(err => {console.log(err)});
}
In order for axios.post() to return a response, in order to perform .then(), you'll need to add a res.json() to the routing codes.
You should map data into your child.
Change your parent like this:
class ParentComponent extends React.Component {
state = {
data : []
}
componentDidMount() {
this.getData();
}
getData = () => axios.get("link/").then(res => this.setState({data: res.data});
deleteData = (id) => axios.get("deleteLink/" + id).then(res => this.getData())
.catch(err => { console.log(err) });
render() {
return (
<div>
<table>
<thead></thead>
<tbody>
{this.state.data.map(x => <ChildComponent row={x} deleteData={this.deleteData} />)}
</tbody>
</table>
</div>
)
}
}
And your child component should be like this
const ChildComponent = ({row,deleteData}) => (
<tr>
<td>{row.id}</td>
<td>{row.name}</td>
<td><button onClick={() => deleteData(row.id)}>Delete</button></td>
</tr >
)
I can't find an issue in your code, the only way I can help is to tell you how I would debug it.
edit parent like so:
class ParentComponent extends React.Component {
state = {
data : []
}
componentDidMount() {
// You are right when you say this should works, so
// stick to it until the bug is fixed
console.log("parent mounted");
this.getData();
}
// retrieves array of data from db
getData = () => {
console.log("fetching data");
axios.get("link/").then(res => {
console.log("fetched data", res.data);
this.setState({data: res.data});
});
}
render() {
return (
<div>
<ChildComponent
data={this.state.data}
refetch={this.getData}
/>
</div>
)
}
}
and in the child component add these 2 lifecycle methods just for debugging purposes:
componentDidMount () {
console.log("child mounted", this.props.data)
}
componentDidUpdate (prevProps) {
console.log("old data", prevProps.data);
console.log("new data", this.props.data);
console.log("data are equal", prevProps.data === this.props.data);
}
if you can share the logs I can try help you more
import React from 'react';
export class Books extends React.Component {
state = {
loading: true,
books: []
}
componentDidMount() {
fetch('**url**')
.then(res => res.json())
.then((data) => {
this.setState({ books: data })
})
.catch(console.log)
}
render() {
const { books } = this.state;
console.log("this.state contains:", this.state);
/* this.state contains: {loading:true, books: status:'success',data:(10)[{id:1, name:xyz,..},{}..] */
const b = books.data;
return (
<div>
{b.map((k) => (
<h1>{k.name}</h1>
))}
</div >
);
}
}
I want to display all the values(for example: id:1, name:xyz, id:2 , name:xxx)
Since 1 week I'm trying to fix this. But couldn't. getting map is not a function.
Please help me to fix this issue.
try following:
let b = books.data || [];
since you're getting data in componentDidMount, there's fair chance it doesn't get the result when you want to render.
The books object in the state is initially empty, therefore books.data does not exist and hence books.data.map is not function. So do conditional render
const {data} = this.state.books
<div>
{data && data.map((k) => (<h1>{k.name}</h1>))}
</div >
Try making a condition to check if b is an array or not! like this:
const b = books.data;
return (
<div>
{
if(Array.isArray(b)){
b.map((k) => (
<h1>{k.name}</h1>
))
}
}
</div >
);
}
I stuck in this moment creating store with different products, that I want to add to the basket. The problem occur when I wanted to pass the state of cardList into Basket component to change the information from "Basket is empty" to display information how many items are currently in basket.
Below I paste my main hooks component with basket component which include all functionality.
Basket component:
import React from 'react'
const Basket = (props) => {
return (
<div>
{props.cardItems.length === 0 ? "Basket is empty" : <div> You have {props.cardItems.length} products in basket!</div>}
</div>
)
}
export default Basket;
Main component:
function
const [cardItems, setCardItems] = useState([]);
const price = 2.50;
useEffect(() => {
fetch(URL, {
method: 'GET',
headers: {
Accept: "application/json",
}
}).then(res => res.json())
.then(json => (setBeers(json), setFilteredBeers(json))
);
}, [])
function handleMatches(toMatch) {...
}
const displayFilterBeers = event => {...
}
const handleRemoveCard = () => {...
}
const handleAddToBasket = (event, beer) => {
setCardItems(state => {
let beerAlreadyInBasket = false;
cardItems.forEach(item => {
if (item.id === beer.id) {
beerAlreadyInBasket = true;
item.count++;
};
});
if (!beerAlreadyInBasket) {
cardItems.push({ ...beer, count: 1 })
}
localStorage.setItem('baketItems', JSON.stringify(cardItems));
console.log('cardItems: ', cardItems, cardItems.length);
return cardItems;
})
}
return (
<div className="App">
<div className='search'>
<input type='text' placeholder='search beer...' onChange={displayFilterBeers} />
</div>
<BeerList BeersList={filteredBeers} price={price} handleAddToBasket={handleAddToBasket} />
<Basket cardItems={cardItems}/>
</div>
);
}
export default App;
I saw an example that without React hooks that in basket component someone used const {cartItems} = this.props; but I don't know how to achieve something similar using hooks.
I think what you are facing is related to this issue.
So when dealing with array or list as state, react doesn't re-render if you don't set state value to a new instance. It assumes from the high-level comparison that the state hasn't been changed. So it bails out from the re-rendering.
from the issue I found this solution is better than the others -
const handleAddToBasket = (event, beer) => {
const nextState = [...cardItems, beer] // this will create a new array, thus will ensure a re-render
// do other stuffs
setCardItems(nextState);
};
I’m new to React and right now I’m working on a project where a user should be able to choose a base ingredient, and it gets added to an array. By clicking on another base ingredient the first one should be removed from the array. Right now the chosen ingredient only removes when clicking on the same one.
I want it to be removed when clicking on another one. Please help :)
import React from 'react';
import Actions from '../../../actions/actions';
import BaseIngredientButton from './BaseIngredientButton';
class BaseIngredientItem extends React.Component {
_OnClick (props) {
if (this.props.isChosen) {
Actions.removeItem(this.props.baseIngredient);
} else {
Actions.addItem(this.props.baseIngredient)
Actions.setBaseIngredient( this.props.baseIngredient );
}
console.log(this.props.isChosen)
}
render () {
return (
<BaseIngredientButton isChosen={this.props.isChosen} onClick={ this._OnClick.bind(this)} txt={ this.props.baseIngredient.name } />
)
}
}
BaseIngredientItem.propTypes = {
baseIngredient: React.PropTypes.object.isRequired,
isChosen: React.PropTypes.bool
}
export default BaseIngredientItem;
here is my store.js
let _cart = [];
const _removeItem = ( item ) => {
_cart.splice( _cart.findIndex( i => i === item ), 1 );
console.log("Ingredients in cart after removal", _cart);
};
const _getItemInCart = ( item ) => {
return _cart.find( ingredient => ingredient.name === item.name )
};
const _addItem = ( item ) => {
if (!_getItemInCart( item )) {
_cart.push(item);
}
console.log("Ingredients in cart after addition", _cart);
};
let _currentBaseIngredient = {};
const _setCurrentBaseIngredient = ( baseIngredient ) => {
_currentBaseIngredient = baseIngredient
};
here is my action.js
addItem( item ){
dispatch({
actionType: AppConstants.ADD_ITEM,
item
})
},
removeItem( item ){
dispatch({
actionType: AppConstants.REMOVE_ITEM,
item
})
},
setBaseIngredient( baseIngredient ){
dispatch({
actionType: AppConstants.SET_BASE_INGREDIENT,
baseIngredient
})
},
Your BaseIngredientItem component has no knowledge of whether there is another base ingredient in the array, so as I mentioned in the comment, this would definitely be something to inspect at the Store level.
Is there any way to determine whether an item is of type base? If there is, you can check for its presence in your addItem function:
(please don't mind some of the psuedo-code)
const _addItem = ( item ) => {
if (item is a base ingredient)
removeCurrentBaseIngredient()
if (!_getItemInCart( item )) {
_cart.push(item);
}
console.log("Ingredients in cart after addition", _cart);
};
const removeCurrentBaseIngredient = () => {
_removeItem( _currentBaseIngredient );
};
Since the store already knows about the _currentBaseIngredient, you should be able to look it up pretty easily and call _removeItem to remove it from the _cart.
I hope that helps!