Keep action from function after re-render - React - javascript

Context : I have a side bar, and multiple links in it. For example, if I click on User, the main page will load datas from users and display in a table. If I click on Addresses, same with all addresses.
What I want to do : I want to set a substring on all spans with a length > 30, so the table is not too long when I receive a long description for example.
My code : I created a Layout component as container for my App component like this :
function MyApp({ Component, pageProps }: AppProps) {
return(
<Layout>
<Component {...pageProps} />;
</Layout>
)
}
And I apply my logic for substring in my layout component :
function Layout(props) {
const subString = (spansList) => {
for (const s of spansList) {
if (s.textContent.length > 30) {
return s.textContent = s.textContent.substring(0, 30) + "..."
}
}
}
const allWithClass = Array.from(
document.getElementsByClassName('MuiTypography-body2')
);
useEffect(() => {
subString(allWithClass)
}, [allWithClass])
return (
[...]
My problem : This code works, but when I re-render by clicking on an other menu in my sidebar, the function subString is no longer applied.
I don't now how to make it persistent.
Thank you for your time

I think what you need is a state which changes depending on the link you click on it, and set this state as a dependence to the useEffect hook, so when you click other link the useEffect hook re-execute

Related

How to use same page anchor tags to hide/show content in Next.js

The problem
The current project is using Next.js and this situation occurred: the content needs to be hidden or replaced, matching the current category selected. I want to do it without reloading or using another route to do so. And when the user presses F5 or reloads the page the content remains unchanged.
The attempts
Next.js' showcase page apparently is able to do so. In the docs, there's a feature called 'Shallow routing', which basically gives the possibility to update the URL without realoading the page. That's what i figured out for now. Any clues on how the content is changed to match the category?
Thanks!
You can load the content on the client based on the category passed in the URL fragment (# value) using window.location.hash.
Here's a minimal example of how to achieve this.
import React, { useState, useEffect } from 'react'
const data = {
'#news': 'News Data',
'#marketing': 'Marketing Data',
default: "Default Data"
}
const ShowCasePage = () => {
const router = useRouter()
const [categoryData, setCategoryData] = useState()
const changeCategory = (category) => {
// Trigger fragment change to fetch the new data
router.push(`/#${category}`, undefined, { shallow: true });
}
useEffect(() => {
const someData = data[window.location.hash] ?? data.default // Retrieve data based on URL fragment
setCategoryData(someData);
}, [router])
return (
<>
<div>Showcase Page</div>
<button onClick={() => changeCategory('news')}>News</button>
<button onClick={() => changeCategory('marketing')}>Marketing</button>
<div>{categoryData}</div>
</>
)
}
export default ShowCasePage

React - How to re-render a component using another component?

I have a NavBar component that has a list of dynamically generated links (these links are generated after querying my backend for some categories). These links are stored inside a child component of the NavBar, called DrawerMenu.
The NavBar is a child of the main App.js component.
In my Category component, I have a "delete" function that deletes a category. Once I delete a category I want to remove the link to it in the NavBar. How would I go about doing this?
For further context, my components are given below:
DrawerMenu component
class DrawerMenu extends Component {
state = {
menuItems: [] // Takes a series of objects of the shape { name: "", link: "" }
}
getData = (query) => {
// Query backend for category data and set it to this.state.menuItems
}
componentDidMount() {
this.getData(menuItemsQuery)
}
render() {
const { classes, handleDrawerClose, open } = this.props
const { menuItems } = this.state
const drawer = (classes, handleDrawerClose) => (
<div>
...
{
menuItems.map((menuItem, index) => (
<Link color="inherit" key={index} to={menuItem.link} className={classes.drawerLink} component={RouterLink}>
<ListItem button className={classes.drawerListItem} onClick={handleDrawerClose}>
<ListItemText primary={menuItem.name} />
</ListItem>
</Link>
))
}
...
</div>
)
...
return (
<div>
<Drawer
variant="temporary"
anchor='left'
open={open}
onClose={handleDrawerClose}
classes={{
paper: `${open ? classes.drawerOpen : null} ${!open ? classes.drawerClose : null}`,
}}
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
>
{drawer(classes, handleDrawerClose)}
</Drawer>
</div>
)
}
}
NavBar component
function PrimarySearchAppBar(props) {
return (
<div className={classes.grow}>
...
<DrawerMenu
classes={classes}
handleDrawerClose={handleDrawerClose}
open={open}
/>
...
</div>
)
}
Category component
class Category extends Component {
...
deleteCategory = async () => {
// Code to request backend to delete category
this.props.history.push(`/`)
}
...
}
There are two common ways of doing this: You can either use a state management tool, like Redux or pass your state down the component tree as props.
Redux is often used when several components depend on the same state or when the component that depends on a state is several layers deep, so it would get cumbersome to pass it down as props.
I'll assume your component tree is not very large, so I will create a simple example passing props down the tree.
class DrawerMenu extends Component {
// We're gonna manage the state here, so the deletion
// will actually be handled by this component
state = {
menuItems: [] // Takes a series of objects of the shape { name: "", link: "" }
}
handleDelete = (id) => {
let updatedMenuItem = [...this.state.menuItems]; //Create a copy
updatedMenuItem = updatedMenuItem(item => item.id !== id) // Remove the
deleted item
this.setState({
menuItems: updatedMenuItem
})
}
...
// Then wherever you render the category component
<Category handleDelete = {handleDelete}/> //Pass a reference to the delete method
}
Category Component
class Category extends Component {
...
deleteCategory = async () => {
// Code to request backend to delete category
this.props.handleDelete(categoryId) //Pass the id of the category
this.props.history.push(`/`)
}
...
}
I would suggest reading about state management, it is a core concept in React and you will use it everywhere. Redux and Context API for example.
Not sure why Dennis Vash deleted their answer, they are correct, but perhaps not descriptive enough in the solution.
The way you delete the category is not to call the backend itself from inside the category component, because then the navbar doesn't know that you made a call, but to call a callback that is in an ancestor shared by both the category component and the navbar to delete a category, and then rerequest the categories list from the server. In the example below, this ancestor that is shared is MyCategoriesProvider
Because the category component is likely to be in a much different place (or multiple places) in the tree than the NavBar, it's best to use context.
Honestly, this is a great place for redux, but I'm not going to push redux on you and instead will just demo a Context solution.
// We're going to create a context that will manage your categories
// The only job of this context is to hold the current categories,
// and supply the updating functions. For brevity, I'll just give
// it a handleDelete function.
// Ideally, you'd also store the status of the request in this context
// as well so you could show loaders in the app, etc
import { createContext } from 'react';
// export this, we'll be using it later
export const CategoriesContext = createContext();
// export this, we'll render it high up in the app
// it will only accept children
export const MyCategoriesProvider = ({children}) => {
// here we can add a status flag in case we wanted to show a spinner
// somewhere down in your app
const [isRequestingCategories,setIsRequestingCategories] = useState(false);
// this is your list of categories that you got from the server
// we'll start with an empty array
const [categories,setCategories] = useState([]);
const fetch = async () => {
setIsRequestingCategories(true);
setCategories(await apiCallToFetchCategories());
setIsRequestingCategories(false);
}
const handleDelete = async category => {
await apiCallToDeleteCategory(category);
// we deleted a category, so we should re-request the list from the server
fetch();
}
useEffect(() => {
// when this component mounts, fetch the categories immediately
fetch();
// feel free to ignore any warnings if you're using a linter about rules of hooks here - this is 100% a "componentDidMount" hook and doesn't have any dependencies
},[]);
return <CategoriesContext.Provider value={{categories,isRequestingCategories,handleDelete}}>{children}</CategoriesContext.Provider>
}
// And you use it like this:
const App = () => {
return (
<MyCategoriesProvider>
<SomeOtherComponent>
<SomeOtherComponent> <- let's say your PrimarySearchBar is in here somewhere
<SomeOtherComponent>
</MyCategoriesProvider>
)
}
// in PrimarySearchBar you'd do this:
function PrimarySearchBar(props) => {
const {categories} = useContext(CategoriesContext); // you exported this above, remember?
// pass it as a prop to navbar, you could easily put the useContext hook inside of any component
return <NavBar categories={categories}/>
}
// in your category component you could do this:
class Category extends Component {
render() {
// Don't forget, categoriesContext is the thing you exported way up at the top
<CategoriesContext.Consumer>
{({handleDelete}) => {
return <button onClick={() => handleDelete(this.props.category)}>
}}
</CategoriesContext.Consumer>
}
}
EDIT:
I see you're mixing class and functional components, which is fine. You should check out this article on how to use the context api in either of them - in functional components you typically use a useContext hook, while in class components you'll use a consumer.
I would just refresh the list of categories that come from the server, after the delete request is done.
I'd do it as follows:
I would make the drawer component not so smart, making it receive the list of menuItems.
<DrawerMenu
classes={classes}
handleDrawerClose={handleDrawerClose}
open={open}
items={/* ... */}
/>
This is an important step, because now, to refresh the list of items rendered, you just pass another list. The server-side logic remains disconnected from this component in this way.
I'm not sure where you render the Category components, but supposing it is rendered outside the PrimarySearchAppBar it seems that this menuItems might need to be passed to the components from an upper level. I see 2 solutions:
I'd do the request for the menuItems from the same place where I do the request for the categories:
const App = props => {
const [categories, setCategories] = React.useState([])
const [menuItems, setMenuItems] = React.useState([])
const fetchCategories = useCallback(()=> {
yourApi.getCategories().then(categories => setCategories(categories))
})
const fetchMenuItems = useCallback(() => {
yourApi.getMenuItems().then(menuItems => setMenuItems(menuItems))
})
useEffect(() => {
fetchCategories()
}, [])
useEffect(() => {
fetchMenuItems()
}, [categories])
const handleDeleteCategory = useCallback(idToDelete => {
yourApi.deleteCategory(idToDelete).then(fetchCategories)
})
return (
<div>
<PrimarySearchAppBar menuItems={menuItems}/>
<Categories categories={categories} onDeleteClick={handleDeleteCategory} />
</div>
)
}
you can do the same thing but do it with a provider and using the content API if you do not want to have all the logic here. It is good to have smart/fetches/server-side logic in a top level component and then pass down props to dumb components.
PS.
There is also a nice hook to make fetches easier:
https://github.com/doasync/use-promise
I currently use a custom version of a usePromise hook I found because I added some interesting features. I can share it if you want but I don't want to add noise to the answer.

CMS driven components in NextJS

Currently have a NextJS site with several pages. Each page can have a number of components in any order. These components are driven by a CMS by whatever the end user chooses.
Example, they could have a carousel, followed by a grid, followed by paragraph component.
Each page I get initial props, which will have the list of components for that page, and the data for each component.
Page1.getInitialProps = async () => {
const res = await fetch('some api')
const json = await res.json()
return { data: json }
}
This ultimately reaches a top level parent component that looks at the data and renders the appropriate child component on the page with a switch statement
renderWidgets = (data) => {
const pageWidgets = data.widgets.map((widget) => {
switch(widget.type) {
case WIDGETS.CAROUSEL:
return <Carousel />;
case WIDGETS.GRID:
return <Grid />;
case WIDGETS.PARAGRAPH:
return <Paragraph />;
default:
return <div />;
}
});
return pageWidgets;
}
render() {
return (
<div className="layout__main">
{this.renderWidgets()}
</div>
);
}
My question is if there is a better way to handle this this rendering component process. Each time a brand new widget is created I'll have to make sure to update the switch statement. Is there a better way of rendering dynamic CMS driven components on a page?

React not re-rendering component after page refresh, even if the state changes

I've got really weird problem. I mean, to me, really. This is not some kind of "use setState instead of this.state" problem. I'm working on trip planning App. I'm using ContextAPI which provides login information and user data (logged user trips etc ) to the whole app.
"Schedule" component, showing day-by-day trip schedule is context subscriber.
Now I want to delete one of the trip days (Day, also subscribing to the context is component rendered by Schedule Component on the Schedule list). And then magic happens:
when I do it right after logging in to the app, everything is working fine. But when I'll refresh the page and delete another day, context state (which as I said, holds all the data, including Days on schedule list) changes (checked via developer tools) but Schedule component is not re-rendering itself, so deleted element remains visible.
But as I said, state changes, and everything is working fine without page refresh before deleting operation. Also when I switch to another tab (Schedule is just one of the tabs, I've got for example Costs and Notes tab on the navbar rendering Costs and Notes components), and then go back to the Schedule tab it re-renders and everything is looking fine, component is showing info accordingly to context state.
Code works like this: deletion (context function ) is triggered by delete icon click in Day.js (context subscriber), Day.js is rendered by Schedule.js (context subscriber too).
In Context.js
... context functions ...
//WRAPPER FOR DELETE FUNCTIONS
delete(url, deleteFunc) {
this.requestsApi.request(url, "DELETE", null)
.then(response => {
if(response.status===204) {
deleteFunc();
} else {
this.toggleRequestError(true);
}
})
.catch( err => {
console.log(err);
this.toggleRequestError(true);
});
}
deleteDay = (id, tripId) => {
let newTrip = this.state.userData.trips.find((trip => trip.id ===
tripId));
newTrip.days = newTrip.days.filter(day => day.id !== id)
this.updateTrip(newTrip);
}
updateTrip = (newTrip) => {
let trips = this.state.userData.trips;
this.setState(prevState => ({
userData : {
...prevState.userData,
trips: trips.map(trip => {
if(trip.id !== newTrip.id)
return trip;
else
return newTrip;
})
}
}
));
}
...
In Schedule.js
... in render's return ...
<ul>
{this.trip.days.map((day,index) => {
return <DayWithContext
inOverview={false}
key={index}
number={index}
day={day}
tripId={this.trip.id}
/>
})}
</ul>
....
In Day.js (delete icon)
...
<img
src={Delete}
alt="Delete icon"
className="mr-2"
style={this.icon}
onClick={() => {
this.props.context.delete(
`/days/${this.props.day.id}`,
() => this.props.context.deleteDay(this.props.day.id,
this.props.tripId)
);
}}
/>
...
Anyone is having any idea what's going on and why Schedule can be not re-rendered after deletion done after page refresh? And even if context state changes? Only idea I have for now is that this is some kind of a bug...
//UPDATE
In Schedule component, in function componentWillReceiveProps() I console log props.context.trip[0].day[0] - first day. It's not an object, just plain text, so its not evaluated "as I click on it in the console".
Before I delete it, console logs first item. After I delete it, console logs second item (so now it's first item on the list) so props given to Schedule are changing, but no render is triggered... What the hell is going on there?
//UPDATE 2
I've also noticed that when I switch to Schedule component from another component, it works good (for example I'm refreshing the page on /costs endpoint then click Schedule tab on navbar menu which leads to /schedule). But when I refresh the page when on /schedule endpoint, this "bug" occurs and component is not re-rendering.
//Render from Context:
...
render() {
const {
isLoggedIn,
wasLoginChecked,
userData,
isLogoutSuccesfulActive,
isLogoutUnSuccesfulActive,
isDataErrorActive,
isRequestErrorActive,
isDataLoaded
} = this.state;
const context = {
isLoggedIn,
wasLoginChecked,
isLogoutSuccesfulActive,
isLogoutUnSuccesfulActive,
isDataErrorActive,
isRequestErrorActive,
userData,
isDataLoaded,
requestsApi : this.requestsApi,
toggleLogin : this.toggleLogin,
checkLogin: this.checkLogin,
setUserData: this.setUserData,
loadData: this.loadData,
addTrip: this.addTrip,
delete: this.delete,
deleteDay: this.deleteDay,
deleteActivity: this.deleteActivity,
deleteTrip: this.deleteTrip,
updateTrip: this.updateTrip,
uncheckLogin: this.uncheckLogin,
toggleLogoutSuccesful: this.toggleLogoutSuccesful,
toggleLogoutUnSuccesful: this.toggleLogoutUnSuccesful,
toggleRequestError: this.toggleRequestError,
toggleDataError: this.toggleDataError
};
return (
<Context.Provider value={context}>
{this.props.children}
</Context.Provider>
)
}
}
export const Consumer = Context.Consumer;
-------------Context HOC:
export default function withContext(Component) {
return function ContextComponent(props) {
return (
<Context.Consumer>
{context => <Component {...props} context={context} />}
</Context.Consumer>
);
}
}
You mutate your state.
In this place, you change an object in the state without using setState:
newTrip.days = newTrip.days.filter(day => day.id !== id)
And then in your map function, you always return the same objects (it is already updated).
So, to fix your issue you can try to change your deleteDay method:
deleteDay = (id, tripId) => {
const trip = this.state.userData.trips.find((trip => trip.id ===
tripId));
const newTrip = {...trip, days: trip.days.filter(day => day.id !== id)};
this.updateTrip(newTrip);
}
UPDATE:
You have one more issue in your Schedule component.
You need to get current trip dynamically, don't set it in the constructor:
Try this:
constructor(props) {
super(props);
this.getTrip = this.getTrip.bind(this);
}
getTrip() {
return this.props.context.userData.trips.find(trip => {
return `${trip.id}`===this.props.match.params.id;
});
}
render() {
const trip = this.getTrip();
return (
...
<ul>
{trip.days.map((day,index) => {
return <DayWithContext
inOverview={false}
key={day.id}
number={index}
day={day}
tripId={trip.id}
/>
})}
</ul>
...
)
}

React :getting offsetTop value and accessing the DOM dynamically

I am currently working on a react app,
and I need to implement changes on the screen that occurs when an element is on a certain position on the screen (on certain offsetTopValue),
this should be done dynamically because every element has a unique id that is created dynamically.
(it also contains 3 purecompenets and 2 functional components down the hierarchy
and most props are passed down using context)
index.jsx
class Collection extends Component {
<CollectionContext.Provider
value={{state: this.state, cards: this.props.cards...}}>
.......
ideal: (
<CollectionLayout cards= {this.props.cards ..../>
.......
</CollectionContext.Provider>
}
layout.jsx
componentDidMount () {
this._bindScroll();
}
_handleScroll = () => {
var html = document.documentElement;
this.updateCardsPositions();
}
render(
...
return(
<CollectionContext.Consumer>
{context =>
<Loader
randomSpin= {this.props.randomSpin}
currentTopic = {this.props.currentTopic}
/>}
<CollectionContext.Consumer>
);
)
loader.jsx
export const Loader = ({ randomSpin, currentTopic}) => (
.....
{!context.state.spinning &&
context.cards.map((card, index) => <Card key={index} card=
{card}/>)}
cards.jsx
export const Card = ({ card } **id={card.id}**) => {
return (...);
}
what I do have: I have a unique card id that is passed to each card,
and I can access the cards id in the layout.
I tried using refs but am not yet sure about the proper way
to pass it through all those components and access it in ComponentDidMount()
in the layout.
I tried utilizing the context but it can be accessed only inside render and not on componentDidMount.
I am also not sure how should I use the refs callback:
({(el)=>{inputRef = el}}) with a dynamic value (every card has a unique id that should be refed.
any other suggestions on how to get the offset top values from the layout would help.
I will appreciate any help or a general solution direction with it.
Thank You!

Categories

Resources