Using useRef hook for scroll detection causing re-rendering - javascript

I am using useRef hook in a layout component to detect the scroll value of it's children (pageContent). I am then conditionally displaying a button to scroll back to top.
Functionalities are working as excepted, but when I console.log something inside the pageComponent children (<route.component/>), it's shows me that the component is re-rendering each time the button shows / hide (scrollTop Value crosses 500)
Here is my Layout component :
function Layout() {
const [showBackToTopButton, setShowBackToTopButton] = useState(false);
const pageContentRef = useRef<HTMLDivElement>(null);
function handleScroll() {
const mainContent = pageContentRef.current;
if (mainContent) {
if (mainContent.scrollTop > 500) {
setShowBackToTopButton(true);
} else {
setShowBackToTopButton(false);
}
}
}
function handleClickBackTop() {
const mainContent = pageContentRef.current;
if (mainContent) mainContent.scrollTo({ top: 0, behavior: 'smooth' });
}
useEffect(() => {
const mainContent = pageContentRef.current;
if (mainContent)
mainContent.onscroll = function () {
handleScroll();
};
}, []);
return (
<>
<div className='drawer drawer-mobile'>
<input
id='left-sidebar-drawer'
type='checkbox'
className='drawer-toggle'
/>
<PageContent pageContentRef={pageContentRef} />
<LeftSidebar />
</div>
{showBackToTopButton && (
<Button
className='back-to-top-button absolute right-0 bottom-10'
id='back-to-top'
onClick={handleClickBackTop}
>
<i className='fa-solid fa-arrow-up' />
</Button>
)}
</>
);
}
export default Layout;
and here is the PageContent component :
interface PageContentProps {
pageContentRef: React.RefObject<HTMLDivElement>;
}
function PageContent({ pageContentRef }: PageContentProps) {
return (
<div className='drawer-content flex flex-col '>
<Header />
<main className='flex-1 overflow-y-auto pt-8 px-6' ref={pageContentRef}>
<Suspense fallback={<SuspenseContent />}>
<Routes>
{routes.map((route, key) => {
return (
<Route
key={key}
path={`${route.path}`}
element={<route.component name={route.name} />}
/>
);
})}
{/* Redirecting unknown url to 404 page */}
<Route path='*' element={<NotFound />} />
</Routes>
</Suspense>
</main>
</div>
);
}
export default PageContent;
Is it a normal behaviour considering the useState value ?
I tried using useMemo but I did not manage to solve my issue.
It's not a very big deal since the component are not rendering tenths of times, but it would be cleaner if they could render only once...

The reason is that when PageContent component value change and Layout component rerender.
pageContent component wrap in memo() hook.
export default React.memo(PageContent);
and second, wrap handleScroll function in useMemo hook that returns memorize function.
useMemo(function handleScroll() {
const mainContent = pageContentRef.current;
if (mainContent) {
if (mainContent.scrollTop > 500) {
setShowBackToTopButton(true);
} else {
setShowBackToTopButton(false);
}
}
}, [dependency]);

I found the solution wrapping my PageContent component inside a memo
const PageContent = React.memo(({ pageContentRef }: PageContentProps) => {
return (
// ...
);
});
Children are now rendered only once.
Hope it can help ohter people 🙂

Related

Can you pass in a useState function into a component on initialization?

I'm using the useState hook to manage rendering components on screen. I want to initialize it with a component while passing in the useState function to set the screen into the component.
Here is my App.js. The error I get is in regards to passing a function into itself on initialization.
function App() {
//useState hooks to determine which component should render
const [screenLoaded, loadScreen] = useState(() => {
<Home setLoadedScreen = {loadScreen}/>
})
return (
<div className="App">
{screenLoaded}
</div>
);
}
The default value for useState is always in the parentheses, no curly braces are needed in this case. const [state, setState] = useState(default). This state could be change in the future with setState(new value).
one simple method is to give your screen a name for example
<Home /> === "home-screen"
<About /> === "about-screen"
so when you pass the setState method of loadScreen into the component, you can switch them by setting the string, for example if you're in home screen and you want to switch to about screen, you'd write
setLoadedScreen("about-screen")
function App(){
const [screenLoaded, loadScreen] = useState("home-screen")
return (
<div className="App">
{screenLoaded === "home-screen" && <Home setLoadedScreen = "loadedScreen" />}
{screenLoaded === "about-screen" && <About setLoadedScreen = "loadedScreen" />}
</div>
);
}

React: How to pass data to children

I am a beginner in React and what I am doing might not make sense.
What I am trying to do is just to pass a data to from a parent component which is used in every screen to children.
My code is like this.
AppDrawer.tsx
const AppDrawer: React: FC<Props> = ({
children,
}) => {
const [aString, setString] = React.useState<string>('Hello');
...
<div>
<Drawer>
...
</Drawer/>
<main>
<div>
<Container>
{children}
</Container>
</div>
</main>
</div>
App.tsx
<Swith>
<AppDrawer>
<Route path="/" component={ChildCompoent} />
...
...
</AppDrawer>
</Switch>
ChildComponent.tsx
export default class ChildComponent extends React.Component<Props, State> {
state = {
..
}
}
And now I want to access aString in AppDrawer.tsx in child components but I couldn't figure out how I can do it. How can I do it?
I think you can use Context here. Context allows you to pass down something from the parent component, and you can get it at the child component (no matter how deep it is) if you'd like.
I made an example link
You can read more about it here
Updated: I notice you use Route, Router, and I don't use in my codesandbox. However, it's fine though. The main idea is to use context :D
Use render in component Route
<Route
path='/'
render={(props) => (
<ChildCompoent {...props}/>
)}
/>
And should not props aString in component AppDrawer
I'm not sure about the version of react and if it is still supported but this how I did this in my app.
try checking for React. Children over here: https://reactjs.org/docs/react-api.html#reactchildren
see if it's suitable for your case.
render() {
const children = React.Children.map(this.props.children, child => {
return React.cloneElement(child, {
...child.props,
setHasChangesBeenMade: (nextValue) => this.setState({ hasChangesBeenMade: nextValue })
});
});
return (children[0]);
}
for you it should be something like this :
const AppDrawer: React: FC<Props> = ({
children,
}) => {
const [aString, setString] = React.useState<string>('Hello');
...
const childrens = React.Children.map(children, child => {
return React.cloneElement(child, {
...child.props,
aString: aString
});
});
<div>
<Drawer>
...
</Drawer/>
<main>
<div>
<Container>
{childrens[0]}
</Container>
</div>
</main>
</div>

DOM render not triggering after updating hooks

I am trying to delete an item (const removeItem) from a list using an onClick event in React.
The state is managed with hooks. I know my way of deleting the item is not the right way yet (i'm putting the name to null), but this is not the issue.
After i set a user to null (i update the users object), i expect a render to happen (useEffect) but it does not. If i switch components and go back to this one, it works, but when clicking the X button, nothing happens in the view.
component Home:
import React, { Suspense, useState, useEffect } from "react";
const Home = props => {
const [users, setUsers] = useState(props.users);
console.log(users);
useEffect(() => {}, [users]);
const addItem = e => {
users.push(e);
console.log(e);
e.preventDefault();
};
const removeItem = item => {
users.forEach(user => {
if (user.id === item) {
user.name = null;
}
});
console.log(users);
setUsers(users);
};
return (
<div className="Home">
<form className="form" id="addUserForm">
<input
type="text"
className="input"
id="addUser"
placeholder="Add user"
/>
<button className="button" onClick={addItem}>
Add Item
</button>
</form>
<ul>
{users.map(item => {
return (
<>
<li key={item.id}>{item.name + " " + item.count}</li>
<button className="button" onClick={() => removeItem(item.id)}>
X
</button>
</>
);
})}
</ul>
</div>
);
};
export default Home;
How i get my users object:
import React from "react";
const LotsOfUsers =[...Array(100).keys()].map((item, key) => item = {
name : `User ${key}`,
id: key,
count: 0
})
export default LotsOfUsers;
main App:
import React from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link,
useRouteMatch,
useParams
} from "react-router-dom";
import Home from "./Home"
import CTX from './store'
import LotsOfUsers from "./LotsOfUsers";
export default function App() {
return (
<CTX.Provider value={{}}>
<Router>
<div>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/topics">Topics</Link>
</li>
</ul>
<Switch>
<Route path="/about">
<About />
</Route>
<Route path="/topics">
<Topics />
</Route>
<Route path="/">
<Home users={LotsOfUsers} text="hello world"/>
</Route>
</Switch>
</div>
</Router>
</CTX.Provider>
);
}
function About() {
return <h2>About</h2>;
}
function Topics() {
let match = useRouteMatch();
return (
<div>
<h2>Topics</h2>
<ul>
<li>
<Link to={`${match.url}/components`}>Components</Link>
</li>
<li>
<Link to={`${match.url}/props-v-state`}>
Props v. State
</Link>
</li>
</ul>
{/* The Topics page has its own <Switch> with more routes
that build on the /topics URL path. You can think of the
2nd <Route> here as an "index" page for all topics, or
the page that is shown when no topic is selected */}
<Switch>
<Route path={`${match.path}/:topicId`}>
<Topic />
</Route>
<Route path={match.path}>
<h3>Please select a topic.</h3>
</Route>
</Switch>
</div>
);
}
function Topic() {
let { topicId } = useParams();
return <h3>Requested topic ID: {topicId}</h3>;
}
Looking into your code, I've noticed 2 times that you change users without the use of setUsers.
const addItem = e => {
users.push(e); // <--- here
console.log(e);
e.preventDefault();
};
const removeItem = item => {
users.forEach(user => {
if (user.id === item) {
user.name = null; // <--- here
}
});
console.log(users);
setUsers(users);
};
In that way, you are updating your users, without letting react know about it. On both cases you have to update users with setUsers and not mutating the array directly.
const addItem = e => {
setUsers(users.concat(e)); // sidenote: if e is your event, then you might be looking for e.target.value here
console.log(e);
e.preventDefault();
};
const removeItem = item => {
setUsers(users.filter(user => user.id !== item));
};
Both .concat() and .filter() use your users array, and return a new array based on the changes you want to apply, and then is used by setUsers to update your users.
So, in extend what I'm actualy doing on removeItem is:
const removeItem = item => {
const newUsers = users.filter(user => user.id !== item); // users remains untouched
setUsers(newUsers);
};
Also, you don't need useEffect hook for this scenario to work properly.
I hope this solves your problem.
You are committing THE Fundamental sin of the react universe. "Mutation"
of state.
const removeItem = item => {
users.forEach(user => {
if (user.id === item) {
user.name = null; // <--- HERE
}
});
console.log(users);
setUsers(users);
};
Check this codesandbox for a working demo of this issue.
https://codesandbox.io/s/jovial-panini-unql4
The react reconciler checks to see whether the 2 objects are equal and since you have just mutated and set the value it registers as the same object and there will be no state change triggered. Hence the view will not be re-rendered and useEffect will not be triggered.

Update state between different Components on ReactJS

I am trying to update the state from an other component to an other component.
I want on header.jsx the state total to be updated when i click on add to cart button on product.jsx
Here is my code
index.jsx
import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import Header from './header';
import Footer from './footer';
import Posts from './posts';
import Post from './post';
import Products from './products';
import Product from './product';
import Page from './page';
// Load the Sass file
require('./style.scss');
const App = () => (
<div id="page-inner">
<Header />
<main id="content">
<Switch>
<Route exact path={Settings.path + 'products/:product'} component={Product} />
</Switch>
</main>
<Footer />
</div>
);
// Routes
const routes = (
<Router>
<Route path="/" component={App} />
</Router>
);
render(
(routes), document.getElementById('page')
);
header.jsx
import React from "react";
import { Link } from "react-router-dom";
class Header extends React.Component {
constructor(props) {
super(props);
this.state = { products: [], total: 0 }
var total = 0;
this.cartUpdated = this.cartUpdated.bind(this)
}
componentDidMount() {
//let cart = localStorage.getItem('total');
// this.setState({ total: 100 });
}
cartUpdated()
{
this.setState({ total: cart+100 });
}
render() {
return (
<div className="cart-icon p-3 m-auto">
Cart/ Total: <span className=""><span className="cart">€</span>{this.state.total}</span><i className="fas fa-shopping-cart" />
</div>
);
}
}
export default Header;
product.jsx
import React from "react";
import NotFound from "./not-found";
import "react-image-gallery/styles/scss/image-gallery.scss";
import ImageGallery from 'react-image-gallery';
class Product extends React.Component {
constructor(props) {
super(props);
this.state = { product: {}, total: 0
};
// ACTIONS
// addToCart
this.addToCart = this.addToCart.bind(this);
}
addToCart()
{
this.props.cartUpdated;
/*
let total = localStorage.getItem('total')
? JSON.parse(localStorage.getItem('total')) : {};
localStorage.setItem('total', 100); */
}
componentDidMount() {
this.fetchData();
}
fetchData = () => {
.......
};
renderProduct() {
if (this.state.product.images) {
const images = [];
if (this.state.product) {
this.state.product.images.map((image, i) => {
var new_image = {"original":image, "thumbnail":image} ;
images.push(new_image);
});
}
return (
<div className="col-md-12">
<div className="row">
<div className="col-md-6">
<ImageGallery items={images} showPlayButton={false} showFullscreenButton={false} thumbnailPosition="left" />
</div>
<div className="col-md-6">
<h4 className="card-title">{this.state.product.name}</h4>
<p className="card-text">
<strike>${this.state.product.regular_price}</strike>{" "}
<u>${this.state.product.sale_price}</u>
</p>
<p className="card-text">
<small className="text-muted">
{this.state.product.stock_quantity} in stock
</small>
</p>
<p
className="card-text"
dangerouslySetInnerHTML={{
__html: this.state.product.description
}}
/>
<div className="superflex add_to_cart_wrapper">
<button type="submit" name="add-to-cart" value={93} className="add_to_cart btn btn-success alt" onClick={this.props.cartUpdated}>Add to cart</button>
</div>
</div>
</div>
</div>
);
}
}
renderEmpty() {
return <NotFound />;
}
render() {
return (
<div className="container post-entry">
{this.state.product ? this.renderProduct() : this.renderEmpty()}
</div>
);
}
}
export default Product;
You can do something like this to achieve this. But the more clean solution which is suggested by the React JS is to use the React Context API.
Am sharing a link from the React JS documentation which exactly have the same scenario that you want to tackle.
https://reactjs.org/docs/context.html#updating-context-from-a-nested-component
And also since you are using the React pure component function so we can use the React hooks, you can have a look here at
https://reactjs.org/docs/hooks-reference.html#usestate
so in your code it should be like this
./Total-Context.js
export const TotalContext = React.createContext({
total: 0,
setTotal: () => {
},
});
./index.jsx
import { TotalContext } from './Total-Context';
const App = () => {
const [total, setTotal] = useState(0);
return (
<TotalContext.Provider value={{total, setTotal}}>
<div id="page-inner">
<Header currentTotal={total} />
<main id="content">
<Switch>
<Route
exact
path={`${Settings.path}products/:product`}
component={Product}
/>
</Switch>
</main>
<Footer />
</div>
</TotalContext.Provider>
);
};
and Now we can use the TotalContext consumer in the Product component and call the method to set the total method in the global context like this.
./Product.jsx
import { TotalContext } from './Total-Context';
const Product = () => (
<TotalContext.Consumer>
{({total, setTotal}) => (
<button
onClick={() => {setTotal(newTotal)}}
>
Update total
</button>
)}
</TotalContext.Consumer>
)
so after calling the click method the Header component should have the updated value of the total.
you can use redux to manage state across multiple components.
getting started with redux
To Answer this:
There is no way by the help of which you can pass state between two react components, as state is private to a component.
props can help you in this regard. Props also can't be passed from child to parent it can always be from parent to child.
There is a twist by the help of which you can achieve this, please follow the below article section: "How to pass Props from child to parent Component?" to get clear idea on this:
URL: https://www.robinwieruch.de/react-pass-props-to-component/

React Router 4 - Controlled Component not Un-Mounting on Switch

I'm trying to learn how to lift the state from <Child/> to <Parent/> and have the parent control user interactions done the child, (which receives state down as a prop) i.e show color and text.
I was able to lift the state up. However, when I switch back and forth between routes the <Parent/> component is not re-mounting and its state remains exactly how it was previously set by setState({})
const cars = [
{ name: "Ferrari", cost: "$9.000", color: "red", id: 1 },
{ name: "Porsche", cost: "$8.000", color: "black", id: 2 },
***
];
class Dealership extends Component {
state = {
cars,
isShow: {},
correctIndex: Math.floor(Math.random() * (cars.length - 1))
};
handleShuffle = () => {
this.setState({
cars: [...this.state.cars.sort(() => Math.random() - 0.5)],
isShow: {}
});
};
handleShow = car => {
const { isShow } = this.state;
console.log("isShow=", isShow);
this.setState(currentState => ({
isShow: { ...currentState.isShow, [car]: true }
}));
};
render() {
return (
<>
<Navigation />
<Routes
state={this.state}
shuffle={this.handleShuffle}
handleShow={this.handleShow}
// isShow={this.state.isShow}
/>
</>
);
}
}
export default withRouter(Dealership);
As mentioned above, the child <Car/>is receiving state down as props so that its user interaction can be controlled by one source of truth the parent <Dealership />
export default class Car extends Component {
render() {
const { cars, shuffle, isShow, handleShow, correctIndex } = this.props;
const correctCar = cars[correctIndex];
const car = cars.map(car => (
<CarList
// {...this.state}
isShow={isShow[car.name]}
key={car.id}
car={car.name}
guess={car.cost}
isCorrect={correctCar.cost === car.cost}
handleShow={handleShow}
/>
));
return (
<>
<Question key={correctCar.id} guess={correctCar.cost} />
<button
onClick={() => {
shuffle();
}}
>
go again
</button>
<ul className="car-list">{car}</ul>
</>
);
}
}
The <CarList/> is abstracted here:
// CarList.js
export const CarList = ({ isShow, isCorrect, car, handleShow, guess }) => {
function getColor() {
if (isShow) {
const showColor = isCorrect ? "green" : "red";
return showColor;
}
return "";
}
return (
<li onClick={() => handleShow(car)} className={getColor()}>
{car}
<span className={isShow ? "show" : "hide"}>{guess}</span>
</li>
);
};
Oddly (to me), when I switch to a route that holds its own local state i.e <Bike/>, everything works as expected (the state is back to original)
import React, { useState } from "react";
export const Bike = () => {
const [color, setColor] = useState(false);
function ChangeColor() {
setColor(true);
}
return (
<p onClick={ChangeColor}>
Click on the <span className={color ? "red" : " "}>Bike</span>
</p>
);
};
This is how I have my Routes setup:
// Navigation.JS
export const Navigation = () => (
<nav>
<ul>
<li>
<Link to="/">home</Link>
</li>
<li>
<Link to="/car-cost">car</Link>
</li>
<li>
<Link to="/bike">bike</Link>
</li>
</ul>
</nav>
);
// Routes.js
export const Routes = ({ state, shuffle, handleShow, isShow }) => (
<Switch>
<Route
path="/car-cost"
render={() => (
<Car
{...state}
shuffle={shuffle}
handleShow={handleShow}
// isShow={isShow}
/>
)}
/>
<Route path="/bike" render={() => <Bike />} />
<Route path="/" component={Home} />
</Switch>
);
I then wrapped my main app with <BrowserRouter /> as you see in totality plus the current misbehavior happening on this code sandbox
How can I switch between routes having <Car/> behave such as <Bike/>? i.e return to its original state. Also, am I lifting and controlling state correctly here?
Here the state are being saved in parent component. When the route changes then only child components are being remounted. So the state of parent component remains there throughout that routing.
You can keep the state in child component, which would reset the state after every unmount. However if you want to lift the state up and still reset the state, then you would have to do that in parent component.
A better way would be to monitor the route change in the parent component. If the route has changed then parent component should reset its state. In componentDidUpdate method of parent component, you can track the route change and reset the state like this
componentDidUpdate(prevProps) {
if (this.props.location.pathname !== prevProps.location.pathname) {
console.log('Route change! Reset the state');
this.setState({ isShow: {}})
}
}

Categories