How can I scroll to a component by component key prop? - reactjs

I have a parent component that renders the following child components:
Object.values(eventData).map((event) => {
return (
<EventsSection
key={event.id}
eventID={event.id}
eventDate={event.date}
/>
);
})
Assuming there are 10-20 records in eventData - upon a certain user action, how can i make the browser window to scroll onto an EventSection records based on its key prop?

You need to use a ref from the id. Something like the below would be enough if the user wanted to click on the list and make the browser scroll to that ref.
function renderEventSection(props) {
const { key, name } = props
const ref = useRef()
const handleClick = () =>
ref.current.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
return (
<div key={key} ref={ref} onClick={handleClick}>
{name}
</div>
)
}
If you need to scroll from outside the list, just move that logic to the parent component or use a React Context (See second example). Also, if you need another event, just change it to any other desired user interaction.
Example for the code above (Click on any of the Data list item)
Example with the scroll from the parent component (Click on any of the buttons and window will scroll to the desired number)

Related

Child state not updated while child props updated

I have a Parent component which generates (and renders) a list of Child components like this:
const Parent= ({ user }) => {
const [state, setState] = useState ({selectedGames: null
currentGroup: null})
...
let allGames = state.selectedGames ? state.selectedGames.map(g => <Child
game={g} user={user} disabled={state.currentGroup.isLocked && !user.admin} />) : null
...
return ( <div> {allGames} </div>)
}
The Child component is also a stateful component, where the state (displayInfo) is only used to handle a toggle behaviour (hide/display extra data):
const Child = ({ game, user, disabled = false }) => {
const [displayInfo, setDisplayInfo] = useState(false)
const toggleDisplayInfo = () =>
{
const currentState = displayInfo
setDisplayInfo(!currentState)
}
...
<p className='control is-pulled-right'>
<button class={displayInfo ? "button is-info is-light" : "button is-info"} onClick = {toggleDisplayInfo}>
<span className='icon'>
<BsFillInfoSquareFill />
</span>
</button>
</p>
...
return ({displayInfo ? <p> Extra data displayed </p> : null})
}
When state.selectedGames is modified (for example when a user interacts with the Parent component through a select), state.selectedGames is correctly updated BUT the state of the i-th Child component stay in its previous state. As an example, let's say:
I clicked the button from the i-th Child component thus displaying "Extra data displayed" for this i-th Child only
I interact with the Parent component thus modifying state.selectedGames (no common element between current and previous state.selectedGames)
I can see that all Child have been correctly updated according to their new props (game, user, disable) but the i-th Child still has its displayInfo set to true (its state has thus not been reset contrary to what I would have (naively) expected) thus displaying "Extra data displayed".
EDIT:
After reading several SO topics on similar subjects, it appears using a key prop could solves this (note that each game passed to the Child component through the game props has its own unique ID). I have also read that such a key prop is not directly expose so I'm a bit lost...
Does passing an extra key prop with key={g.ID} to the Child component would force a re-rendering?

React scroll to element (ref current is null) problem

I have a problem I'm not able to solve. The app got a component where a do looping array and making multiple elements off it. Then I want to make buttons in another component that will scroll to a specific element. (something similar to liveuamap.com when you click on a circle).
I tried the below solution, but got "Uncaught TypeError: props.refs is undefined". I could not find any solution to fix it.
The second question: is there a better or different solution to make scrolling work?
In app component I creating refs and function for scrolling:
const refs = DUMMY_DATA.reduce((acc, value) => {
acc[value.id] = React.createRef();
return acc;
}, {});
const handleClick = (id) => {
console.log(refs);
refs[id].current.scrollIntoView({
behavior: "smooth",
block: "start",
});
};
The refs I send to the article component as a prop where I render elements with generated refs from the app component.
{props.data.map((article) => (
<ContentArticlesCard
key={article.id}
ref={props.refs[article.id]}
data={article}
onActiveArticle={props.onActiveArticle}
activeArticle={props.activeArticle}
/>
))}
The function is sent to another component as a prop where I create buttons from the same data with added function to scroll to a specific item in the article component.
{props.data.map((marker) => (
<Marker
position={[marker.location.lat, marker.location.lng]}
icon={
props.activeArticle === marker.id ? iconCircleActive : iconCircle
}
key={marker.id}
eventHandlers={{
click: () => {
props.onActiveArticle(marker.id);
// props.handleClick(marker.id);
},
}}
></Marker>
))}
Thanks for the answers.
Ok so i found the solution in library react-scroll with easy scroll implementation.

How can you prevent propagation of events from a MUI (v4) Slider Component

I have a MUI v4 Slider (specifically used as a range slider: https://v4.mui.com/components/slider/#range-slider) component inside an expandable component in a form, however, the onChange handler for the Slider component immediately propagates up into the parent and triggers the onClick handler which controls the hide/show.
In the child:
import { Slider } from '#material-ui/core';
export const MySliderComponent = ({ setSliderValue }) => {
let onChange = (e, value) => {
e.stopPropagation();
setSliderValue(value);
}
return <Slider onChange={onChange} />
}
In the parent:
let [expanded, setExpanded] = useState(false);
let toggle = (e) => setExpanded(!expanded);
return (
<React.Fragment>
<div className={'control'} onClick={toggle}>Label Text</div>
<div hidden={!expanded}>
<MySliderComponent />
</div>
</React.Fragment>
);
Points:
when I click inside the slider component, but not on the slider control, it does not trigger the toggle in the parent
when I click on the slider control, the event immediately (on mouse down) triggers the toggle on the parent
throwing a e.preventDefault() in the onChange handler has no effect
using Material UI v4 (no I can't migrate to 5)
I don't understand why the onChange would trigger the parent's onClick. How do I prevent this, or otherwise include a Slider in expandable content at all?
Edit:
After further debugging I found that if I removed the call to setSliderValue, that the parent did not collapse/hide the expanded content. Then I checked the state of expanded and it seems to be resetting without a call to setExpanded. So it looks like the parent component is re-rendering, and wiping out the state of the useState hook each time.
Following solution worked for me in a simmilar problem:
Give your parent component a unique id (for readability)
div id="parent" className={'control'} onClick={toggle}
Modify the parent's onClick handler (toggle):
let toggle = (e) => {
if (wasParentCLicked()) setExpanded(!expanded);
function wasParentCLicked() {
try {
if (e.target.id === "parent") return true;
} catch(error) {
}
return false;
}
}
For further help refer to the official documentation of the Event.target API: https://developer.mozilla.org/en-US/docs/Web/API/Event/target

What is the best approach for adding class and styles without re-render - reactjs

I am building a slider and need assistance with the "best" way of implementing the feature. I have a Slider Component which receives children of SliderItems. I clone the children in Slider Component and add props. When the user clicks next or previous button I use a state isAnimating to determine if the slider is moving and add/remove styles based on isAnimating state but it was causing a re-render of slider items. I need to add animating class without causing a re-render to the enter slide items. Is there a way to implement such feature?
SliderContainer.js
<Slider totalItems={totalItems} itemsInRow={itemsInRow} enableLooping={true} handleSliderMove={handleSliderMove}>
{items.map((item) => {
return <SliderItem key={\`${item.id}\`} data={item} />;
})}
</Slider>
Slider.js
const onSliderControlClick = (direction) => {
const [newIndex, slideOffset] = sliderMove(direction, lowestVisibleIndex, itemsInRow, totalItems);
setisAnimating(true); //Causes rerender
movePercent.current = slideOffset();
setTimeout(() => {
ReactDOM.unstable_batchedUpdates(() => {
setisAnimating(false);
setHasMovedOnce(true);
setLowestVisibleIndex(newIndex());
});
}, 750);
};
<div ref={sliderContent} className={`slider-content`} style={getReactAnimationStyle(baseOffset)}>
React.Children.map(children, (child, i) =>
React.cloneElement(child, {
key: child.props.video.id,
viewportIndex: properties.viewportIndex,
viewportPosition: properties.viewportPosition,
})
);
})
</div>
use node-sass library. And make styles.modules.scss file for styling, write down css classes. And conditionally you can apply classes on any element like this.
for example -
className={props.count > 2 ? classes.abc : classes.xyz}

Not able to click list child element from side menu using react testing library

It's my first week using the react library and I'm a bit confused with this piece where I have a list of items that are part of a side menu. I can assert their texts identifying them as the child of the parent List tag. But when I try to fire a click event this way it won't navigate me to the next page. It will only work if I target that list item by their text directly, not as the child of the parent list, which I'm trying to avoid because there may be other clickable elements with that same title on the page. If anyone could please point me towards what I may be lacking in understanding here please.
describe('App when it is rendered in a certain state:', () => {
beforeAll(() => {
render(<Root/>);
});
it('should render the dashboard after logging in', async () => {
expect(screen.queryByTestId('dashboard')).toBeInTheDocument();
expect(screen.getByTestId('side-menu')).toBeInTheDocument();
const liArr = screen.queryAllByRole('listitem')
expect(liArr[0].textContent).toBe('Dashboard')
expect(liArr[1].textContent).toBe('Schools')
expect(liArr[2].textContent).toBe('Teachers')
const teachersLink = liArr[2];
expect(teachersLink).toBeInTheDocument();
// await fireEvent.click(screen.getByText('Teachers')) // This navigates me fine
await fireEvent.click(teachersLink); // This does not navigate me
expect(screen.getByText('Teachers page')).toBeInTheDocument();
});
});
This is the SideMenu component where the links come from:
export default function SideMenu() {
const classes = useStyles();
return (
<div className={classes.root} data-testid="side-menu">
<List>
<ListItemLink to="/dashboard" primary="Dashboard"/>
<ListItemLink to="/schools" primary="Schools"/>
<ListItemLink to="/teachers" primary="Teachers"/>
</List>
</div>
);
}
Thanks very much.
It seems it was not working because the link item was actually not clickable, not being an anchor element. Changed it to the below piece and it worked, warranting uniqueness through the role of a button.
async function navigateToTeachersPage() {
await fireEvent.click(screen.getByRole('button', { name: 'Teachers' }));
expect(screen.getByTestId('teachers')).toBeInTheDocument();
}

Resources