How to use MDCRipple.attachTo on multiple buttons in React Component - javascript

I have a simple React component that renders multiple buttons from an array in my props. I'm applying the ripple on DidMount, however, it's only attaching on the first button, the rest are being ignored. It looks like the attachTo only takes the first element. Is there another way to attach to all the buttons on didmount?
class NavBar extends Component {
constructor(props) {
super(props);
this.state = {
links
};
}
componentDidMount() {
MDCRipple.attachTo(document.querySelector('.mdc-button'));
}
render() {
return (
<section>
{this.state.links.map((link, i) => {
return (
<StyledLink key={i} to={link.url}>
<StyledButton className="mdc-button">
<StyledIcon className="material-icons">{link.icon}</StyledIcon>
<StyledTypography className="mdc-typography--caption">
{link.title}
</StyledTypography>
</StyledButton>
</StyledLink>
);
})}
</section>
);
}
}
Final markup
<a class="sc-iwsKbI bhaIR">
<button class="mdc-button sc-dnqmqq ksXmjj mdc-ripple-upgraded" style="--mdc-ripple-fg-size:57.599999999999994px; --mdc-ripple-fg-scale:2.1766951530355496; --mdc-ripple-fg-translate-start:-7.799999999999997px, 19.200000000000003px; --mdc-ripple-fg-translate-end:3.200000000000003px, 19.200000000000003px;">
...content
</button>
</a>
<a class="sc-iwsKbI bhaIR">
<button class="mdc-button sc-dnqmqq ksXmjj">
...content
</button>
</a>
Updated
I was able to find a way to use the attachTo with each button, but it still seems like there's a better way.
I changed by componentDidMount() to:
componentDidMount() {
this.state.links.forEach((link) => {
MDCRipple.attachTo(document.getElementById(`button-navbar-${link.id}`));
});
}
and then changed my render to
<StyledButton id={`button-navbar-${link.id}`} className="mdc-button">
Is there a way to do this without having to iterate through the array?

The react way to do this is to write component that injects the necessary logic.
class RippleButton extends Component {
const handleRef = elem => MDCRipple.attachTo(elem);
render() {
return (
<StyledButton {...this.props} ref={this.handleRef} />
);
}
}
Then render that component instead of your original StyledButton component and it will call the MDCRipple.attachTo() itself with its ref.
Depending on how the StyledButton is implemented you may need to use another prop to get the ref to the underlying DOM element. You did not provide enough of your code to exactly know this.

Related

How to re-render a component with React-Router <Link> pointing to the same URL

To keep it simple, the detail page fetches data on mount based on the movie ID in the URL, this coming from path='movie/:id' in the Route.
It's child is called Recommended, which shows you recommended movies based again on the current URL.
class MovieDetailPage extends React.Component {
// Fetch movies and cast based on the ID in the url
componentDidMount() {
this.props.getMovieDetails(this.props.match.params.id)
this.props.getMovieCast(this.props.match.params.id)
}
render() {
<div>
Movies here
</div>
<Recommended id={this.props.match.params.id}/>
}
}
The Recommended component fetches data based on the current movie as well and generates another tag pointing to another movie.
class Recommended extends React.Component {
componentDidMount() {
this.props.getRecommended(this.props.id)
}
render() {
return (
<>
<Category title={'Recommended'}></Category>
<div className="movies">
{
this.props.recommended.map((movie) => {
return (
<Link key={movie.id} to={`movie/${movie.id}`} className="movies__item">
<img
key={movie.id}
src={`https://image.tmdb.org/t/p/w342${movie.poster_path}`}
className="movies__item-img"
alt={`A poster of ${movie.title}`}
>
</img>
</Link>
)
})
}
</div>
</>
)
}
}
Now how can I trigger another render of the parent component when clicking the Link generated in the Recommended component? The URL is changing but this won't trigger a render like I intent to do.
UPDATE:
<Route
path="/movie/:id"
render={(props) => (
<MovieDetailPage key={props.match.params.id}
{...props}
)}
/>
I passed in a unique key this time that triggered the re-render of the page. I tried this before but I might've screwed up the syntax.
This post got me in the right direction: Force remount component when click on the same react router Link multiple times
Add a key to the page
If you change route but your page is not getting its "mount" data then you should add a key to the page. This will cause your page to rerender and mount with the new id and get the data again.
You can read more about react keys here
A key tells react that this is a particular component, this is why you see them in on lists. By changing the key on your page you tell react that this is a new instantiation of the component and has changed. This will cause a remount.
Class component example
class MyPage extends React.Component {
componentDidMound() {
// this will fire each time the key changes since it triggers a mount
}
render() {
return (
<div key={props.pageId}>
{/* component stuff */}
</div>
)
}
}
Functional component example
const MyPage = (props) => {
React.useEffect(() => {
// this will fire each time the key changes
}, []);
return (
<div key={props.pageId}>
{/* component stuff */}
</div>
)
}
You can add another React lifecycle method that triggers on receiving new props (UNSAFE_componentWillReceiveProps, componentDidUpdate, getDerivedStateFromProps) in your Recommended component like this:
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.id !== this.props.id) {
nextProps.getRecommended(nextProps.id);
};
}
You can also add key to your component (which forces it to re-render completely if key changed) like this:
<Recommended key={this.props.match.params.id} id={this.props.match.params.id}/>
You can also use React Hooks to handle this more easily with useEffect:
const Recommended = (props) => {
const { id, getRecommended, recommended } = props;
useEffect(() => {
id && getRecommended(id);
}, [id]);
return (
<>
<Category title={'Recommended'}></Category>
<div className="movies">
{recommended.map((movie) => {
return (
<Link key={movie.id} to={`movie/${movie.id}`} className="movies__item">
<img
key={movie.id}
src={`https://image.tmdb.org/t/p/w342${movie.poster_path}`}
className="movies__item-img"
alt={`A poster of ${movie.title}`}
></img>
</Link>
);
})}
</div>
</>
);
};
Note: adding key to component and complete its re-render is not best practice and you should be using Component's lifecycles to avoid it if possible

Changing the state in reactJs parent is not changing the props in react child components

I have a Parent React Component and which have 3 Child components i am changing the state in parent component but changing the state in parent is not changing the props in child components.I am passing the state from parent to child components but the props are not changing inside the child components.
My parent component
class Parent extends Component {
state = {
menuCategoryId:'',
}
handelOnClickRefundMenu = () => {
this.setState({menuCategoryId:''});
}
render() {
return (
<FoodMenu
menuCategories={menuCategories}
{...this.state}
/>
)
}
}
export default Parent;
Child 1 Component
class FoodMenu extends Component {
render() {
return (
<MenuCategories
MenuCategories={menuCategories.MenuCategories}
selectedMenuCategoryId={this.props.menuCategoryId}
/>
);
}
}
export default Child1;
Child 2 component
class MenuCategories extends React.Component{
render(){
const MenuCategories = this.props.MenuCategories;
const selectedMenuCategoryId = this.props.selectedMenuCategoryId;
const renderCategories = (MenuCategories) => (
MenuCategories ?
MenuCategories.map(card=>(
<MenuCategory
key={card._id}
{...card}
handleOnClickMenuCategory={this.props.handleOnClickMenuCategory}
selectedMenuCategoryId={this.props.selectedMenuCategoryId}
// propData={...this.props}
/>
))
:null
)
return (
<Fragment>
<div id="" className="food-menus-menu w-100">
<div className="row">
<OwlCarousel
className="owl-theme"
loop={true}
items={9}
autoplay={false}
autoplayTimeout={3000}
autoplayHoverPause={false}
nav={true}
navElement={'div'}
navText={[`<img src=${seventheenPng}>`,`<img src=${eitheenPng}>`]}
dots={false}
responsive={{
0:{
items:4
},
600:{
items:3
},
1000:{
items:7
}
}}
>
{MenuCategories ?
MenuCategories.length === 0 ?
<div className="no_result">
Sorry, no results
</div>
:null
:null}
{ renderCategories(MenuCategories)}
</OwlCarousel>
</div>
</div>
</Fragment>
)
}
};
export default MenuCategories;
Child 3 Component
class MenuCategory extends Component {
render() {
const props = this.props;
console.log('The values of the props are not changing here')
console.log(props.selectedMenuCategoryId)
return (
<div className={`colCategory item`} onClick={()=>props.handleOnClickMenuCategory(props)}>
<button
className={`btn btn-primary w-100 py-2 d-inline-flex align-items-center justify-content-center ${props.selectedMenuCategoryId === props._id ? 'activeMenuCategory' : ''}`}>
{props.name}
</button>
</div>
);
}
}
export default MenuCategory;
The value of props "props.selectedMenuCategoryId" in my last component which is inside the Map function MenuCategory is not changing when i change the state in my Parent Class function handelOnClickRefundMenu
The Map function is inside Child Component 2 MenuCategories .
Kindly help me on this please.
Thanks in advance.
All the answers about forcing re-renders using lifecycle methods are wrong. If you pass the props down correctly and they change, your child components should re-render automatically.
To demonstrate this, here's a quick'n'dirty sandbox that has a parent and two children that passes a prop down as you require.
I don't know exactly what's wrong with your code (a self-contained example that we can run and debug would help here), but I suggest paring it back to a simpler case that you can get working and then build up from there.
eta: Are you sure the problem isn't to do with your click handler? You're not passing it in to FoodMenu or MenuCategories.
Why are you naming your prop and variables the same as your component? It’s really hard to read and is probably causing confusion as to whether you’re referencing the component or the prop/variable. Use camel case
Naming your prop MenuCategories inside the component MenuCategories is not only bad practice, but could solve the issue if named menuCategories instead.

How to get a child component to control the parent in react?

I'm building a Modal component and want to make sure that it would easily fit a variety of different possible modal designs.
In my case it would have optional header, content and footer. I don't have an issue with this so far, the code looks like:
class ModalHeader extends React.Component {
render () {
const children = React.Children.map(this.props.children, _.identity);
return (
<div className="modal-header">
{children}
</div>
);
}
}
class ModalFooter extends React.Component {
render () {
const children = React.Children.map(this.props.children, _.identity);
return (
<div className="modal-footer">
{children}
</div>
);
}
}
class ModalContent extends React.Component {
render () {
const children = React.Children.map(this.props.children, _.identity);
return (
<div className="modal-content">
{children}
</div>
);
}
}
class Modal extends React.Component {
render () {
var header, footer, content = null;
React.Children.map(this.props.children, function(child){
if (child.type === ModalHeader) {
header = child;
}
if (child.type === ModalFooter) {
footer = child;
}
if (child.type === ModalContent) {
content = child;
}
});
return (
<div>
{header}
{content}
{footer}
</div>
);
}
}
Now comes the issue of closing the modal. I want it to be possible to close the modal from clicking an element that would be in any of the sub components, no matter, to the left or the right or anything and potentially nested deeply in the markup specific to that component.
Having a component that would wrap a piece of markup be it an X, a close icon or a button, making sure that when that element is clicked, the whole modal is closed.
class ModalCloser extends React.Component {
render () {
const children = React.Children.map(this.props.children, _.identity);
return (
<div onClick={this.close} className="modal-closer">
{children}
</div>
);
}
close () {
// no idea what goes here!!
}
}
There doesn't seem to be in React any easy way for a child component to communicate with the parent.
I would be fine with passing a prop to that closer element that would be the callback function to close the main modal, but at the place where I would be defining the modal, I don't see any way that it would be available:
<Modal>
<ModalHeader>
<div>
<header>
<h1>Title!!</h1>
<div class="float-right">
<ModalCloser closeHandler={???}>
X
</ModalCloser>
</div>
</header>
</div>
</ModalHeader>
<ModalContent>
...
</ModalContent>
</Modal>
Alternatively, I see that I could recursively traverse all the descendants and maybe do something to all descendants of type ModalCloser but I also don't really see a way to do that available.
What would be a good solution allowing to pass such a sub-component as a child or descendant to keep the layout flexibility while giving it the possibility to close the modal in such a case?
Pass a callback from the parent to the child and on close modal, call the callback in the child.
Here is a simpler version of your code, where modalClose is the callback that's being passed to the parent from the child. You can also test it in jsfiddle.
class ModalCloser extends React.Component {
render () {
return (
<div onClick={this.props.close}>
{this.props.children}
</div>
);
}
}
class Main extends React.Component {
modalClose() {
alert('closing')
}
render() {
return (<ModalCloser close={this.modalClose}>X</ModalCloser>);
}
}
ReactDOM.render(
<Main />,
document.getElementById('container')
);
In your jsfiddle you have defined the callback in the render method, therefore it was executed on every rerender of the component. I would strongly recommend you to read Thinking in React multiple times - it will answer all your questions.
Pseudocode
<Parent>
modalClose () {
console.log('modal closed...')
}
<Child modalClose={modalClose} />
<Parent/>
Now in your < Child /> whenever modal closes:
closeHandler = { this.props.modalClose }

Programmatically open a route with state in react

I have two types of item, one of which can contain data similar to the other.
Currently when form is used to save an item it saves it then uses browserHistory.push to show the next page.
But I wish add a button that will
save the currently item
redirect them to the form to add the other item type,
partially fill out this form with the data from the first item.
Is there a way to do this using react and not using local storage or session variables?
You should take a look to Redux (or other Flux based libraries) to store data between components and routes, avoiding the excessive prop nesting.
browserHistory.push won't work. It only moves you to a certain location but it doesn't update the application state. You need to update application state, which then will reflect into location update, but not in the opposite direction. Keep in mind that, in React, data comes first, and its representation, even though mutable, doesn't change the data back. The same applies to the location.
To make the redirect alone work, I'd recommend wrapping your component into withRouter higher-order component.
import React, { Component } from 'react';
import { withRouter } from 'react-router';
class MyComponent extends Component {
render() {
return (
<div>
<button
onClick={() => this.props.router.push('/new-location')}>
Click me to go to /new-location
</button>
</div>
);
}
}
But if you need to pass data from one component to another, and the two aren't in hierarchy, I'd agree with Alomsimoy and recommend using Redux. But if, for some reason, it's not an option, you can store this data in a component that is parent to both forms:
class FormA extends Component {
render() {
return (
<form onSubmit={() => this.props.onSubmit()}>
<input
type="text"
value={this.props.inputA}
onChange={(event) => this.props.handleChangeA(event)} />
</form>
);
}
}
class FormB extends Component {
render() {
return (
<form onSubmit={() => this.props.onSubmit()}>
<input
type="text"
value={this.props.inputB}
onChange={(event) => this.props.handleChangeB(event)} />
</form>
);
}
}
while their parent would rule the location and state updates:
class Forms extends Component {
constructor() {
super();
this.state = {};
}
handleChange(name, value) {
this.setState({
[name]: value
});
}
renderForm() {
const {
params: {
stepId
}
} = this.props;
if (stepId === 'step-a') { // <- will be returned for location /form/step-a
return (
<FormA
inputA={this.state.inputA}
handleChangeA={(event) => this.handleChange('inputA', event.target.value)}
onSubmit={() => this.props.router.push('/form/step-b')} />
);
} else if (stepId === 'step-b') { // <- will be returned for location /form/step-b
return (
<FormB
inputB={this.state.inputB}
handleChangeB={{(event) => this.handleChange('inputA', event.target.value)} />
);
}
}
render() {
const {
children
} = this.props;
console.log(this.state); // track changes
return (
<div>
{this.renderForm()}
<button
onClick={() => this.props.router.push('/new-location')}>
Click me to go to /new-location
</button>
</div>
);
}
}
export default withRouter(Forms);
so the route for them would look like
<Route path="form/:stepId" component={Forms} />

Toggle Class in React

I'm using react for a project where I have a menu button.
<a ref="btn" href="#" className="btn-menu show-on-small"><i></i></a>
And a Sidenav component like:
<Sidenav ref="menu" />
And I wrote the following code to toggle the menu:
class Header extends React.Component {
constructor(props){
super(props);
this.toggleSidenav = this.toggleSidenav.bind(this);
}
render() {
return (
<div className="header">
<i className="border hide-on-small-and-down"></i>
<div className="container">
<a ref="btn" href="#" className="btn-menu show-on-small"><i></i></a>
<Menu className="menu hide-on-small-and-down"/>
<Sidenav />
</div>
</div>
)
}
toggleSidenav() {
this.refs.btn.classList.toggle('btn-menu-open');
}
componentDidMount() {
this.refs.btn.addEventListener('click', this.toggleSidenav);
}
componentWillUnmount() {
this.refs.btn.removeEventListener('click', this.toggleSidenav);
}
}
The thing is that this.refs.sidenav is not a DOM element and I cant add a class on him.
Can someone explain me how to toggle class on the Sidenav component like I do on my button?
You have to use the component's State to update component parameters such as Class Name if you want React to render your DOM correctly and efficiently.
UPDATE: I updated the example to toggle the Sidemenu on a button click. This is not necessary, but you can see how it would work. You might need to use "this.state" vs. "this.props" as I have shown. I'm used to working with Redux components.
constructor(props){
super(props);
}
getInitialState(){
return {"showHideSidenav":"hidden"};
}
render() {
return (
<div className="header">
<i className="border hide-on-small-and-down"></i>
<div className="container">
<a ref="btn" onClick={this.toggleSidenav.bind(this)} href="#" className="btn-menu show-on-small"><i></i></a>
<Menu className="menu hide-on-small-and-down"/>
<Sidenav className={this.props.showHideSidenav}/>
</div>
</div>
)
}
toggleSidenav() {
var css = (this.props.showHideSidenav === "hidden") ? "show" : "hidden";
this.setState({"showHideSidenav":css});
}
Now, when you toggle the state, the component will update and change the class name of the sidenav component. You can use CSS to show/hide the sidenav using the class names.
.hidden {
display:none;
}
.show{
display:block;
}
refs is not a DOM element. In order to find a DOM element, you need to use findDOMNode menthod first.
Do, this
var node = ReactDOM.findDOMNode(this.refs.btn);
node.classList.toggle('btn-menu-open');
alternatively, you can use like this (almost actual code)
this.state.styleCondition = false;
<a ref="btn" href="#" className={styleCondition ? "btn-menu show-on-small" : ""}><i></i></a>
you can then change styleCondition based on your state change conditions.
Toggle function in react
At first you should create constructor
like this
constructor(props) {
super(props);
this.state = {
close: true,
};
}
Then create a function like this
yourFunction = () => {
this.setState({
close: !this.state.close,
});
};
then use this like
render() {
const {close} = this.state;
return (
<Fragment>
<div onClick={() => this.yourFunction()}></div>
<div className={close ? "isYourDefaultClass" : "isYourOnChangeClass"}></div>
</Fragment>
)
}
}
Please give better solutions
Ori Drori's comment is correct, you aren't doing this the "React Way". In React, you should ideally not be changing classes and event handlers using the DOM. Do it in the render() method of your React components; in this case that would be the sideNav and your Header. A rough example of how this would be done in your code is as follows.
HEADER
class Header extends React.Component {
constructor(props){
super(props);
}
render() {
return (
<div className="header">
<i className="border hide-on-small-and-down"></i>
<div className="container">
<a ref="btn" href="#" className="btn-menu show-on-small"
onClick=this.showNav><i></i></a>
<Menu className="menu hide-on-small-and-down"/>
<Sidenav ref="sideNav"/>
</div>
</div>
)
}
showNav() {
this.refs.sideNav.show();
}
}
SIDENAV
class SideNav extends React.Component {
constructor(props) {
super(props);
this.state = {
open: false
}
}
render() {
if (this.state.open) {
return (
<div className = "sideNav">
This is a sidenav
</div>
)
} else {
return null;
}
}
show() {
this.setState({
open: true
})
}
}
You can see here that we are not toggling classes but using the state of the components to render the SideNav. This way, or similar is the whole premise of using react. If you are using bootstrap, there is a library which integrates bootstrap elements with the react way of doing things, allowing you to use the same elements but set state on them instead of directly manipulating the DOM. It can be found here - https://react-bootstrap.github.io/
Hope this helps, and enjoy using React!
For anybody reading this in 2019, after React 16.8 was released, take a look at the React Hooks. It really simplifies handling states in components. The docs are very well written with an example of exactly what you need.

Categories

Resources