I have an app that lists books on a users shelf and then on a subsequent search page.
The user goes to the search page, finds a title and selects a shelf for the title to be shelved on. When they go back to the home page this title should then show on the correct shelf.
The functionality works in that the changes are made to the objects, but when I click on the home button or back button in the browser the changes do not show until I have refreshed the browser.
What do I need to do to ensure this change is shown when the user browses to the home page?
I've put the bulk of the code into Codesandbox
App.js
import React, { Component } from 'react'
import ListBooks from './ListBooks'
import SearchBooks from './SearchBooks'
import * as BooksAPI from './utils/BooksAPI'
import { Route } from 'react-router-dom'
class BooksApp extends Component {
state = {
books: []
}
componentDidMount() {
BooksAPI.getAll()
.then((books) => {
this.setState(() => ({
books
}))
})
}
updateShelf = (book, shelf) => {
const bookFromState = this.state.books.find(b => b.id === book.id);
if (bookFromState) {
// update existing
bookFromState.shelf = shelf;
this.setState(currentState => ({
books: currentState.books
}));
} else {
// add new one
this.setState(prevState => ({
books: prevState.books
}));
}
BooksAPI.update(book, shelf);
};
render() {
return (
<div>
<Route exact path='/' render={() => (
<ListBooks
books={this.state.books}
onUpdateShelf={this.updateShelf}
/>
)} />
<Route exact path='/search' render={() => (
<SearchBooks
books={this.state.books}
onUpdateShelf={this.updateShelf}
/>
)} />
</div>
)
}
}
export default BooksApp
So I checked your code. You had problem updating your state actually. The books wasn't changing after you selected a book from within the search screen. Here's the code from your App.js:
updateShelf = (book, shelf) => {
console.log(book, shelf)
const bookFromState = this.state.books.find(b => b.id === book.id);
if (bookFromState) {
// update existing
bookFromState.shelf = shelf;
this.setState(currentState => ({
books: currentState.books
}));
} else {
// add new one
// the following lines of yours were different
book.shelf = shelf;
this.setState(prevState => ({
books: prevState.books.concat(book)
}));
}
BooksAPI.update(book, shelf);
};
So you merely had books: prevState.books instead of actually concatenating the book to the prev state. And just before that the shelf of book has to be changed to the one you pass.
PS: I might have left some console.log statements. Hope that is not a problem and you will clean the mess.
Related
I have page where a user can narrow their search using filters. The issue is that when a user clicks on a filter to filter properties by "rent" or "buy", the url does not refresh to reflect the changes. The changes do however appear in the URL, which is what I want, but I have to manually press enter to refresh the page with the specified filters, so that changes would appear.
As you can see in the photo, the properties listed are not "for rent" properties, so the only way to correctly see the rental properties is to manually enter the url: http://localhost:3000/search?purpose=for-rent&minPrice=20000
import {
Flex,
Select,
Box,
} from "#chakra-ui/react";
import { useRouter } from "next/router";
import Image from "next/image";
import { filterData, getFilterValues } from "../utils/filterData";
class searchFilters extends Component {
constructor(props) {
super(props);
this.state = {
filters: filterData,
};
}
handleChange = (filterValues) => {
const path = this.props.params.pathname;
const { query } = this.props.params;
const values = getFilterValues(filterValues);
values.forEach((item) => {
if (item.value && filterValues?.[item.name]) {
query[item.name] = item.value;
}
});
this.props.params.push({ pathname: path, query: query });
};
render() {
const { filters } = this.state;
return (
<Flex bg="gray.100" p="4" justifyContent="center" flexWrap="wrap">
{filters.map((filter) => (
<Box key={filter.queryName}>
<Select
placeholder={filter.placeholder}
w="fit-content"
p="2"
onChange={(e) =>
this.handleChange({ [filter.queryName]: e.target.value })
}
>
{filter.items.map((item) => (
<option value={item.value} key={item.value}>
{item.name}
</option>
))}
</Select>
</Box>
))}
</Flex>
);
}
}
const withParams = (Component) => {
return (props) => <Component {...props} params={useRouter()} />;
};
export default withParams(searchFilters);
As you are using the same component, it will not reload the page. You can detect the param change with useEffect hook and add the refreshig logic within it. This would reload the data as per the new param.
const { query } = useRouter();
useEffect(() => {
// Refresh logic
}, [query.purpose)]);
so, I'm currently working on a MERN app that successfully saves JWT tokens via library localstorage, surviving any refresh attempts (new users show up in the database, etc, the backend is all working as intended).
The issue is, the frontend React app has 'user' set to 'null' by default in the container's state, so that incongruency is what keeps logging users upon out upon re-rendering despite the JWT. I've been stuck on this for over a day now, have tried implementing a variety of possible solutions, have received help from my instructors, etc, nothing is achieving the desired result- does anyone have any advice?
I have attached the code from my container for reference (excuse the messiness, I'm in the middle of being too frustrated with this whole thing to do much about that), Furthermore I also got a bunch of other components and files that interact with my container in some way or other, won't attach them now but if anyone feels that the extra context is needed in order to help then I will do so. Thank you!
import React, { Component } from "react";
import { getItems } from "../services/items";
import Routes from "../routes";
import Header from "../screens/Header";
import { verifyToken } from '../services/auth'
export default class Container extends Component {
constructor(props) {
super(props);
this.state = {
user: null,
items: [],
isLoggedIn: false
};
}
async componentDidMount() {
// const user = await verifyToken();
// if (user) {
try {
const items = await getItems();
this.setState({
items,
isLoggedIn: true
});
}
catch (err) {
console.error(err);
}
}
addItem = item =>
this.setState({
items: [item, ...this.state.items]
});
editItem = (itemId, item) => {
const updateIndex = this.state.items.findIndex(
element => element._id === itemId
),
items = [...this.state.items];
items[updateIndex] = item;
this.setState({
items
});
};
destroyItem = item => {
const destroyIndex = this.state.items.findIndex(
element => element._id === item._id
),
items = [...this.state.items];
if (destroyIndex > -1) {
items.splice(destroyIndex, 1);
this.setState({
items
});
}
};
setUser = user => this.setState({ user });
//verifyUser = user => (localStorage.getItem('token')) ? this.setState({ user, isLoggedIn: true }) : null
clearUser = () => this.setState({ user: null });
render() {
// const token = localStorage.getItem('token');
// console.log(token)
const { user, items } = this.state;
return (
<div className="container-landing">
<Header user={user} />
<main className="container">
<Routes
items={items}
user={user}
setUser={this.setUser}
addItem={this.addItem}
editItem={this.editItem}
destroyItem={this.destroyItem}
clearUser={this.clearUser}
//verifyUser={this.verifyUser}
/>
</main>
</div>
);
}
}
I have a Main.js component, that route to various components, including Listing.js.
Main: it contains as state an array of the products added to the cart.
List: it's a listing of the products. It contains as state all the products from database.
My problem: when I add a product to the cart by clicking on a button in List component, it adds the product to the cart, updating the state cart of Main component. Doing so, the List component rerenders, and I loose all the filters the visitor seted on the listing.
I'd like to prevent List from rerender when cart state of its parent component changes. Do you have any idea of how to do that ? I've tried with shouldComponentUpdate, but not succesfull.
Main.js (parent component)
import React from 'react';
import {Switch, Route, withRouter, Link} from "react-router-dom";
import {List} from "./Listing/List";
class Main extends React.Component
{
state={
cart:[],
};
removeFromCart = product => //REMOVES PRODUCT FROM CART
{
let cart = this.state.cart;
cart.map(item => {
if(item._id === product._id)
{
item.count--;
return item;
}
});
cart = cart.filter(item => item.count > 0);
this.setState({cart:cart}, () => {sessionStorage.setItem('cart', JSON.stringify(cart));});
};
addToCart = product => //ADD PRODUCT TO CART
{
let cart = this.state.cart;
let productExists = this.state.cart.map(item => {return item._id === product._id}).includes(true);
if(productExists)
{
cart = cart.map(item => {
if(item._id === product._id)
{
item.count++;
return item;
}
else
{
return item;
}
});
}
else
{
product.count = 1;
cart.push(product);
}
this.setState({cart: cart}, () => {sessionStorage.setItem('cart', JSON.stringify(cart));});
};
componentWillMount()
{
if(sessionStorage.getItem('cart')) this.setState({cart:JSON.parse(sessionStorage.getItem('cart'))});
}
render() {
return (
<div className='main'>
<Header cart={this.state.cart} />
<Switch>
<Route path='/listing' component={() => <List addToCart={this.addToCart} />} />
</Switch>
</div>
);
}
}
export default withRouter(Main);
List.js, listing of product:
import React from "react";
import {Product} from "./Product";
import Data from '../../Utils/Data';
import {Search} from "./Search/Search";
export class List extends React.Component
{
state = {
serie: '',
products: [],
filteredProducts: [],
};
addToCart = this.props.addToCart;
obtainProducts = (request = {}) => //searches for products in database
{
Data.products.obtain(request).then(products => {
this.setState({products:products, filteredProducts: products});
});
};
displayProducts = () => {
//Only products that has title
const products = this.state.filteredProducts.filter(product => {return product.title;});
//Returns REACT COMPONENT
return products.map(product => {
return <Product
key={product._id}
product={product}
addToCart={this.addToCart}
/>
});
};
searchHandler = (collection, types) =>
{
let filteredProducts = this.state.products;
if(collection.length)
filteredProducts = filteredProducts.filter(product => {return collection.includes(product.series);});
if(types.length)
filteredProducts = filteredProducts.filter(product => {return types.includes(product.type);});
this.setState({filteredProducts: filteredProducts});
};
componentWillMount()
{
//init products collection
this.obtainProducts();
}
render()
{
const productComponents = this.displayProducts();
console.log('test');
return(
<section className='listing'>
<Search searchHandler={this.searchHandler} />
<div className='listing-content grid-4 has-gutter'>
{productComponents}
</div>
</section>
)
}
}
If you pass an anonymous function to component prop in Route, it re-renders everytime.
Instead, set your route as :
<Route path='/listing' render={() => <List addToCart={this.addToCart} />} />
Quoting from react router docs:
When you use component (instead of render or children, below) the router uses React.createElement to create a new React element from the given component. That means if you provide an inline function to the component prop, you would create a new component every render. This results in the existing component unmounting and the new component mounting instead of just updating the existing component. When using an inline function for inline rendering, use the render or the children prop
Wrapping elements in anonymous functions in react will cause that element to be re-instantiated on every render (in some cases).
I think the issue lies with how you're using the Route component. Using the children prop might make this more intuitive.
<Route path='/listing'>
<List addToCart={this.addToCart} />
</Route>
I'm new to react and struggling with how to transfer data from one component to another.
I referred some tutorials and blogs, but things aren't working for me.
I have two child components, Body-content.jsx and Profile.jsx and 1 parent component parent.jsx
I want to transfer some data from Body-content.jsx toProfile.jsx.
Here's my code
Body-content.jsx
class BodyContent extends React.Component {
componentDidMount() {
this.getUserList()
}
getUserList(){
fetch('https://jsonplaceholder.typicode.com/users')
.then(result => {
return result.json();
}).then(data =>{
this.setState({
users : data
})
})
}
render() {
const user = this.state.users.map((userData, i) => (
<CardBody>
...some code here
<Button color='primary' onClick={e => this.viewProfile(userData)}>View Profile</Button>
</CardBody>
</Card>
));
return (
<>
<div>{user}</div>
</>
)
}
viewProfile = function (data) {
}
}
export default BodyContent;
profile.jsx
class Profile extends React.Component {
componentDidMount() {
}
render() {
return (
<>
<TopNav />
<main className="profile-page" ref="main">
<section>
//code goes here
</section>
</main>
</>
);
}
}
export default Profile;
Store your data in parent component and send it as props to children.
If you have to change it in one of them, then send (also as prop) the function, which will change data in parent component.
Code sample:
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {someData: ''};
}
changeData(newData) {
this.setState({
someData: newData
});
}
render() {
return (
<>
<Child1 setData={this.changeData} data={this.state.someData} />
<Child2 setData={this.changeData} data={this.state.someData} />
</>
)
}
}
Both of them will be able to change data in parent component using this.props.setData('newData')
If you want to share your state across the child component then you may need to move that property in parent's state which you may able to share between two child components.
Sibling to Sibling
Parent Component
Any to Any
Observer Pattern
Global Variables
Context
How to make a shared state between two react components?
You can hoist state to parent component:
class Parent extends Component {
state = {
users
};
handleUsersChange = users => this.setState({ users });
render() {
const { users } = this.state;
return (
<React.Fragment>
<Body-content onUsersChange={ this.handleUsersChange } />
<Profile users={ users } />
</React.Fragment>
);
}
}
...
class BodyContent extends React.Component {
getUserList(){
fetch('https://jsonplaceholder.typicode.com/users')
.then(result => {
return result.json();
}).then(data =>{
this.props.handleUsersChange(data);
})
}
}
In ReactJs the data flow is uni-directional - Top-to-bottom. The parents passes the data to respective children.
Here, since you want to share the data between two siblings. The data should be first available to their parent.
In order to do so, your getUserList api call should be inside of your parent, i.e
your parent.jsx component.
From there you can pass the data as props to both the siblings.
Let me know if you need and further explanation to this. If needed, please share your parent.jsx code. I can help you further.
Hi and welcome to the world of React,
If you want to share data between siblings components, you should always keep in mind that you should store your data at the highest common component (doc).
In your case that would mean having a parent component that holds your users list and the current profile in its state, and then render accordingly your list and the current profile.
A little example to get you on the "right track" sandbox:
class Parent extends Component {
constructor(props) {
super(props);
this.state = {
users: [],
currentIndex: null
}
}
componentDidMount() {
this.getUserList()
}
getUserList(){
fetch('https://jsonplaceholder.typicode.com/users')
.then(result => result.json())
.then(data => {
this.setState(() => ({
users : data
}))
});
}
updateCurrent = (index) => {
this.setState(() => ({ currentIndex: index }));
}
render() {
return (
<div>
<UserList
users={this.state.users}
updateCurrent={this.updateCurrent}
/>
{this.state.currentIndex !== null && (
<Profile user={this.state.users[this.state.currentIndex]} />
)}
</div>
)
}
}
const UserList = (props) => (
<div>
{props.users.map((user, i) => (
<div key={user.id}>
<button color='primary' onClick={() => props.updateCurrent(i)}>View Profile</button>
</div>
))}
</div>
);
const Profile = ({ user }) => (
<div className="profile-page">
{user.name}
</div>
);
Feel free to ask clarification if needed.
Happy coding,
As practice using react, react-router, and redux, I am making a very small app that has two models: vacuums and stores. In my app you can see all vacuums, all stores, a single vacuum or a single store. You can add and delete instances of vacuums and stores form the single page view.
The problem I am having is that after deleting a vacuum or store, my app breaks because the route no longer exists.
I tried adding a NotFound component and route but because I am using variables inside of routes (/vacuums/:id), it does get there.
<Switch>
<Route exact path="/stores" component={Allstores} />
<Route exact path="/stores/add" component={storesAddForm} />
<Route exact path="/stores/:storeId" component={Singlestores} />
<Route exact path="/vacuums" component={Allvacuums} />
<Route exact path="/vacuums/add" component={vacuumsAddForm} />
<Route exact path="/vacuums/:vacuumsId" component={Singlevacuums} />
<Route component={NotFound} />
</Switch>
Delete function using redux:
export const REMOVE_VACUUM = 'REMOVE_VACUUM';
export const removeVacuum = vacuumId => {
return {
type: REMOVE_VACUUM,
vacuumId: vacuumId,
};
};
const deleteVacuum = id => {
return async (dispatch, getState) => {
const deleted = await axios.delete(`/api/allVacuums/${id}`);
dispatch(removeVacuum(id));
};
};
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case REMOVE_VACUUM:
const arr = state.vacuums.filter(
vacuum => vacuum.id !== action.vacuumId
);
return { ...state, vacuums: arr };
}
}
Since you're using redux, from your Singlevacuums component pass this.props.history from the component to your deleteVacuum action creator:
import React, { PureComponent } from 'react';
import { deleteVacuum } from '../actions/vacuumActions';
class Singlevacuums extends PureComponent {
handleClick = id => {
const { deleteVacuum, history } = this.props;
deleteVacuum(id, history); // delete vacuum action action creator
}
render = () => (
<div>
{this.props.vacuums.map(({ id }) => (
<button id={id} onClick={() => this.handleClick(id)}>Delete Vacuum</button>
))}
</div>
)
}
export default connect(state=> ({ vacuums: state.vacuums }), { deleteVacuum })(Singlevacuums)
Then in your action creator (always try/catch your promises, otherwise, when they fail -- and they will -- your app won't crash!!!):
const deleteVacuum = (id, history) => async dispatch => {
try {
await axios.delete(`/api/allVacuums/${id}`); // remove vacuum from API
dispatch(removeVacuum(id)); // remove vacuum from redux state
history.push('/'); // redirect user to "/" or where ever
catch (err) {
console.error(err);
}
};