How to change React component from outside? - reactjs

The React component, which can be considered as third-party component, looks as following:
import * as React from 'react';
import classnames from 'classnames';
import { extractCommonClassNames } from '../../utils';
export const Tag = (props: React.ElementConfig): React$Node =>{
const{
classNames,
props:
{
children,
className,
...restProps
},
} = extractCommonClassNames(props);
const combinedClassNames = classnames(
'tag',
className,
...classNames,
);
return (
<span
className={combinedClassNames}
{...restProps}
>
{children}
<i className="sbicon-times txt-gray" />
</span>
);
};
The component where I use the component above looks as following:
import React from 'react';
import * as L from '#korus/leda';
import type { KendoEvent } from '../../../types/general';
type Props = {
visible: boolean
};
type State = {
dropDownSelectData: Array<string>,
dropDownSelectFilter: string
}
export class ApplicationSearch extends React.Component<Props, State> {
constructor(props) {
super(props);
this.state = {
dropDownSelectData: ['Имя', 'Фамилия', 'Машина'],
dropDownSelectFilter: '',
};
this.onDropDownSelectFilterChange = this.onDropDownSelectFilterChange.bind(this);
}
componentDidMount() {
document.querySelector('.sbicon-times.txt-gray').classList.remove('txt-gray');
}
onDropDownSelectFilterChange(event: KendoEvent) {
const data = this.state.dropDownSelectData;
const filter = event.filter.value;
this.setState({
dropDownSelectData: this.filterDropDownSelectData(data, filter),
dropDownSelectFilter: filter,
});
}
// eslint-disable-next-line class-methods-use-this
filterDropDownSelectData(data, filter) {
// eslint-disable-next-line func-names
return data.filter(element => element.toLowerCase().indexOf(filter.toLowerCase()) > -1);
}
render() {
const {
visible,
} = this.props;
const {
dropDownSelectData,
dropDownSelectFilter,
} = this.state;
return (
<React.Fragment>
{
visible && (
<React.Fragment>
<L.Block search active inner>
<L.Block inner>
<L.Block tags>
<L.Tag>
option 1
</L.Tag>
<L.Tag>
option 2
</L.Tag>
<L.Tag>
...
</L.Tag>
</L.Block>
</L.Block>
</React.Fragment>
)}
</React.Fragment>
);
}
}
Is it possible to remove "txt-gray" from the component from outside and if so, how?

Remove the class from where you're using the Tag component:
componentDidMount() {
document.querySelector('.sbicon-times.txt-gray').classList.remove('txt-gray')
}
Or more specific:
.querySelector('span i.sbicon-times.txt-gray')
As per your comment,
I have multiple components with "txt-gray", but when I use your code, "txt-gray" has been removed from first component only. How to remove it from all components?
I will suggest you to use the code to remove the class in the parent component of using the Tag component. And also use querySelectorAll as in this post.

Refactoring
A clean way is to modify the component to allow it to conditionally add txt-gray through a prop:
<i className={classnames('sbicon-times', { 'txt-gray': props.gray })} />
If the component cannot be modified because it belongs to third-party library, this involves forking a library or replacing third-party component with its modified copy.
Direct DOM access with findDOMNode
A workaround is to access DOM directly in parent component:
class TagWithoutGray extends React.Component {
componentDidMount() {
ReactDOM.findDOMNode(this).querySelector('i.sbicon-times.txt-gray')
.classList.remove('txt-gray');
}
// unnecessary for this particular component
componentDidUpdate = componentDidMount;
render() {
return <Tag {...this.props}/>;
}
}
The use of findDOMNode is generally discouraged because direct DOM access is not idiomatic to React, it has performance issues and isn't compatible with server-side rendering.
Component patching with cloneElement
Another workaround is to patch a component. Since Tag is function component, it can be called directly to access and modify its children:
const TagWithoutGray = props => {
const span = Tag(props);
const spanChildren = [...span.props.children];
const i = spanChildren.pop();
return React.cloneElement(span, {
...props,
children: [
...spanChildren,
React.cloneElement(i, {
...i.props,
className: i.props.className.replace('txt-gray', '')
})
]
});
}
This is considered a hack because wrapper component should be aware of patched component implementation, it may break if the implementation changes.

No, it is not possible
The only way is to change your Tag component
import * as React from 'react';
import classnames from 'classnames';
import { extractCommonClassNames } from '../../utils';
export const Tag = (props: React.ElementConfig): React$Node =>{
const{
classNames,
props:
{
children,
className,
...restProps
},
} = extractCommonClassNames(props);
const combinedClassNames = classnames(
'tag',
className,
...classNames,
);
const grayClass = this.props.disableGray ? 'sbicon-times' : 'sbicon-times txt-gray';
return (
<span
className={combinedClassNames}
{...restProps}
>
{children}
<i className={grayClass} />
</span>
);
};
Now, if you pass disableGray={true} it will suppress the gray class, otherwise of you pass false or avoid passing that prop at all it will use the gray class. It is a small change in the component, but it allows you not to change all the points in your code where you use this component (and you are happy with grey text)

Related

React Hooks - Passing ref to child to use ScrollIntoView

I have two components. A parent and a child.
Inside the parent component I have a button. If the user clicks on that button I want to do a ScrollIntoView to another button inside the child component.
I guess I want to define a reference to the childs button a so that I inside the parent button onClick can do a:
ref.scrollIntoView({block: 'end', behavior: 'smooth'});
that will scroll to the button in the child component.
Here is a minified example:
ParentComponent.jsx
import React, {useRef} from 'react';
import ChildComponent from './ChildComponent';
const ParentComponent = props => {
const childReference = useRef(null);
const onClick = () => {
childReference.scrollIntoView({block: 'end', behavior: 'smooth'});
}
return (
<>
<...some other components>
<Button onClick={onClick}>Click me to be forwarded</Button>
<ChildComponent ref={childReference}/>
</>
);
};
ChildComponent.jsx
import React from 'react';
const ChildComponent = (props, ref) => {
const { name, value, description } = props;
return (
<...some other components>
<Button ref={ref}>You should be forwarded to me</Button>
);
};
ChildComponent.propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.number,
description: PropTypes.string,
};
ChildComponent.defaultProps = {
value: 0,
description: '',
};
export default React.forwardRef(ChildComponent);
I know the above code doesn't work, it was just to illustrate what I am trying to achieve.
I have really tried every other solution I have been able to find by Googling and they all seem so easy, but none of them seem to work for my use case. I have tried using forwardRef as well, but that also doesn't fix it for me.
UPDATE
I guess I was a little vague on what's not working. I've been getting a lot of different error messages with the implementation.
The following is one of them:
Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
Solution
Okay. I thought I'd assemble the pieces here with the solution provided by #Vencovsky.
This is the full implementation with the two example components seen in the question:
ParentComponent.jsx
import React, { useRef } from 'react';
import ChildComponent from './ChildComponent';
const ParentComponent = props => {
const childReference = useRef(null);
const scrollIntoView = () => {
childReference.current.scrollIntoView({block: 'center', inline: 'center', behavior: 'smooth'});
}
return (
<>
<...some other component>
<Button onClick={scrollIntoView}>Click me to be forwarded</Button>
<ChildComponent ref={childReference}
</>
);
};
export default ParentComponent;
ChildComponent.jsx
import React, {forwardRef} from 'react';
import PropTypes from 'prop-types';
const ChildComponent = forwardRef((props, ref) => {
const { name, value, description } = props;
return(
<>
<...some other components>
<Button ref={ref}>You should be forwarded to me</Button>
</>
);
});
ChildComponent.propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.number,
description: PropTypes.string,
};
ChildComponent.defaultProps = {
value: 0,
description: '',
};
export default ChildComponent;
Edit 2:
I guess you are doing something like
const ChildComponent = (props, ref) => { ... }
ChildComponent.propTypes = { ... }
export default React.forwardRef(ChildComponent)
But what you need to do is pass propTypes after React.forwardRef, like so:
const ChildComponent = (props, ref) => { ... }
const ForwardComponent = React.forwardRef(ChildComponent)
ForwardComponent.propTypes = { ... }
export default ForwardComponent
A better way to do it would be like
// using forwarRef
const ChildComponent = React.forwarRef((props, ref) => {
const { name, value, description } = props;
return (
<...some other components>
<Button ref={ref}>You should be forwarded to me</Button>
);
});
Then you wouldn't need to change propTypes and create another component.
Edit:
As your Edit, I can see that you forgot to use React.forwardRef.
You should add
export default React.forwardRef(ChildComponent)
To your ChildComponent file (export it with forwardRef).
What is not working? Are you getting an error? You should explain better what is going on, but I will try to guess.
There is somethings that can make it not work.
You need to use ref.current.foo instead of ref.foo
As #JeroenWienk said:
It seems that your button is also a custom component. Are you sure the ref is being passed to the html button element inside there?
To use the second parameter of an functional component, you should be using React.forwardRef. e.g. export default React.forwardRef(ChildComponent)

react context with componentdidupdate

I am running a pattern like so, the assumption is that SearchResultsContainer is mounted and somewhere a searchbar sets the input.
class SearchResults {
render() {
return(
<ResultsContext.Consumer value={input}>
{input => <SearchResultsContainer input=input}
</ResultsContext.Consumer>
)
}
class SearchResultsContainer
componentDidUpdate() {
//fetch data based on new input
if (check if data is the same) {
this.setState({
data: fetchedData
})
}
}
}
this will invoke a double fetch whenever a new context value has been called, because componentDidUpdate() will fire and set the data. On a new input from the results context, it will invoke componentDidUpdate(), fetch, set data, then invoke componentDidUpdate(), and fetch, then will check if data is the same and stop the loop.
Is this the right way to be using context?
The solution I used is to transfer the context to the props through a High Order Component.
I have used this very usefull github answer https://github.com/facebook/react/issues/12397#issuecomment-374004053
The result looks Like this :
my-context.js :
import React from "react";
export const MyContext = React.createContext({ foo: 'bar' });
export const withMyContext = Element => {
return React.forwardRef((props, ref) => {
return (
<MyContext.Consumer>
{context => <Element myContext={context} {...props} ref={ref} />}
</MyContext.Consumer>
);
});
};
An other component that consumes the context :
import { withMyContext } from "./path/to/my-context";
class MyComponent extends Component {
componentDidUpdate(prevProps) {
const {myContext} = this.props
if(myContext.foo !== prevProps.myContext.foo){
this.doSomething()
}
}
}
export default withMyContext(MyComponent);
There must be a context producer somewhere :
<MyContext.Provider value={{ foo: this.state.foo }}>
<MyComponent />
</MyContext.Provider>
Here is a way to do it that doesn't require passing the context through props from a parent.
// Context.js
import { createContext } from 'react'
export const Context = createContext({ example: 'context data' })
// This helps keep track of the previous context state
export class OldContext {
constructor(context) {
this.currentContext = context
this.value = {...context}
}
update() {
this.value = {...this.currentContext}
}
isOutdated() {
return JSON.stringify(this.value) !== JSON.stringify(this.currentContext)
}
}
// ContextProvider.js
import React, { Component } from 'react'
import { Context } from './Context.js'
import { MyComponent } from './MyComponent.js'
export class ContextProvider extends Component {
render(){
return (
<MyContext.provider>
{/* No need to pass context into props */}
<MyComponent />
</MyContext.provider>
)
}
}
// MyComponent.js
import React, { Component } from 'react'
import { Context, OldContext } from './Context.js'
export class MyComponent extends Component {
static contextType = Context
componentDidMount() {
this.oldContext = new OldContext(this.context)
}
componentDidUpdate() {
// Do all checks before updating the oldContext value
if (this.context.example !== this.oldContext.value.example) {
console.log('"example" in context has changed!')
}
// Update the oldContext value if the context values have changed
if (this.oldContext.isOutdated()) {
this.oldContext.update()
}
}
render(){
return <p>{this.props.context.example}</p>
}
}
You could pass just the value that is changing separately as a prop.
<MyContext.Provider value={{ foo: this.state.foo }}>
<MyComponent propToWatch={this.state.bar}/>
</MyContext.Provider>
The extent -> props wrapper seems to a recommended by the react staff. However, they dont seem to address if its an issue to wrap context in a prop for an then consume the context directly from the child of the child, etc.
If you have many of these props you are needing to watch, especially when not just at the ends of branches for the component tree, look at Redux, its more powerful that the built in React.extent.

Render HOC(Component) without changing Component Name in JSX

I have two HOCs that add context to a component like so :
const withContextOne = Component => class extends React.Component {
render() {
return (
<ContextOne.Consumer>
{context => <Component {...this.props} one={context} /> }
</ContextOne.Consumer>
);
}
};
export default withContextOne;
Desired Result
I just want an syntactically concise way to wrap a component with this HOC so that it doesn't impact my JSX structure too much.
What I have tried
Exporting a component with the HOC attached export default withContextOne(withContextTwo(MyComponent)) This way is the most concise, but unfortunately it breaks my unit tests.
Trying to evaluate the HOC from within JSX like :
{ withContextOne(withContextTwo(<Component />)) }
This throws me an error saying
Functions are not valid as a React child. This may happen if you return a Component instead of < Component /> from render.
Creating a variable to store the HOC component in before rendering :
const HOC = withContextOne(Component)
Then simply rendering with <HOC {...props}/> etc. I don't like this method as it changes the name of the component within my JSX
You can set the displayName before returning the wrapped component.
const withContextOne = Component => {
class WithContextOneHOC extends React.Component {
render() {
return (
<ContextOne.Consumer>
{context => <Component {...this.props} one={context} /> }
</ContextOne.Consumer>
);
}
}
WithContextOneHOC.displayName = `WithContextOneHOC(${Component.displayName})`;
return WithContextOneHOC;
};
This will put <WithContextOneHOC(YourComponentHere)> in your React tree instead of just the generic React <Component> element.
You can use decorators to ease the syntactic pain of chained HOCs. I forget which specific babel plugin you need, it might (still) be babel-plugin-transform-decorators-legacy or could be babel-plugin-transform-decorators, depending on your version of babel.
For example:
import React, { Component } from 'react';
import { withRouter } from 'react-router';
import { injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { resizeOnScroll } from './Resize';
#withRouter
#resizeOnScroll
#injectIntl
#connect(s => s, (dispatch) => ({ dispatch }))
export default class FooBar extends Component {
handleOnClick = () => {
this.props.dispatch({ type: 'LOGIN' }).then(() => {
this.props.history.push('/login');
});
}
render() {
return <button onClick={}>
{this.props.formatMessage({ id: 'some-translation' })}
</button>
}
}
However, the caveat with decorators is that testing becomes a pain. You can't use decorators with const, so if you want to export a "clean" undecorated class you're out of luck. This is what I usually do now, purely for the sake of testing:
import React, { Component } from 'react';
import { withRouter } from 'react-router';
import { injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { resizeOnScroll } from './Resize';
export class FooBarUndecorated extends Component {
handleOnClick = () => {
this.props.dispatch({ type: 'LOGIN' }).then(() => {
this.props.history.push('/login');
});
}
render() {
return <button onClick={}>
{this.props.formatMessage({ id: 'some-translation' })}
</button>
}
}
export default withRouter(
resizeOnScroll(
injectIntl(
connect(s => s, ({ dispatch }) => ({ dispatch }))(
FooBarUndecorated
)
)
)
);
// somewhere in my app
import FooBar from './FooBar';
// in a test so I don't have to use .dive().dive().dive().dive()
import { FooBarUndecorated } from 'src/components/FooBar';

How to avoid writing the same logic in components having a different layout?

How can I avoid writing the same code when two components share some same methods but have a different layout?
The sample components below have a method "renderLastItem" which uses prop "something" passed by the parent components.
I thought about using Higher Order Component Pattern but I'm not sure I I can pass props as an argument to Higher Order Component.
The sample code below is very simple, so in this sample code, I just need to use If statement and change the layout according to the type of components, but in real code, I have more codes and I want to avoid using if statement in order to change the layout according to the type of a component.
How can I avoid writing the same logic in multiple components?
ComponentA
import React, { Component } from 'react';
import PropTypes from 'prop-types';
const propTypes = {};
const defaultProps = {};
class SampleA extends Component {
constructor(props) {
super(props);
}
renderLastItem() {
if(!this.props.something) {
return null;
}
return this.props.something[this.props.something.length - 1];
}
render() {
return (
<div>
<h1>Something</h1>
<p>{this.renderLastItem()}</p>
</div>
);
}
}
SampleA.propTypes = propTypes;
SampleA.defaultProps = defaultProps;
export default SampleA;
ComponentB
import React, { Component } from 'react';
import PropTypes from 'prop-types';
const propTypes = {};
const defaultProps = {};
class SampleB extends Component {
constructor(props) {
super(props);
}
renderLastItem() {
if(!this.props.something) {
return null;
}
return this.props.something[this.props.something.length - 1];
}
render() {
return (
<div>
<ul>
<li>Something</li>
<li>Something else</li>
<li>{this.renderLastItem()}</li>
</ul>
</div>
);
}
}
SampleB.propTypes = propTypes;
SampleB.defaultProps = defaultProps;
export default SampleB;
You absolutely can pass props to a Higher-Order Component! A HOC is simply a function that takes a Component as an argument and returns another Component as a result. So you could create a Higher-Order withLastOfSomething Component just like this:
function withLastOfSomething(Component) {
return function({something, ...otherProps}) {
const item = something ? something[something.length - 1] : null;
return <Component item={item} {...otherProps} />;
}
}
Or with ES6 arrow functions, even more compactly like this:
const withLastOfSomething = (Component) => ({something, ...otherProps}) => {
const item = something ? something[something.length - 1] : null;
return <Component item={item} {...otherProps} />;
}
And then use it like this:
const SampleBWithLastOfSomething = withLastOfSomething(SampleB);
return (<SampleBWithLastOfSomething something={...} />);
You can separate the function that takes the passed props and executes the logic,
export default renderLastItem = (passedProps) => {
if(!passedProps) {
return null;
}
return passedProps [passedProps.length - 1]
}
then import it wherever you need, like this:
import renderLastItem from './somewhere'
export default class SampleA extends Component {
render() {
return (
<div>
<h1>Something</h1>
<p>{renderLastItem(this.props.something)}</p>
</div>
)
}
}

React Higher Order Component that detects dom events that takes functional components as arg

I have a scenario where I want to create an HOC that detects mouse events (e.g. mouseenter, mouseleave) when they occur on the HOC's WrappedComponent, then pass the WrappedComponent a special prop (e.g. componentIsHovered). I got this working by using a ref callback to get the wrapped component instance, then adding event listeners to the wrapped instance in my HOC.
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
export default (WrappedComponent) => {
return class DetectHover extends Component {
constructor(props) {
super(props)
this.handleMouseEnter = this.handleMouseEnter.bind(this)
this.handleMouseLeave = this.handleMouseLeave.bind(this)
this.bindListeners = this.bindListeners.bind(this)
this.state = {componentIsHovered: false}
this.wrappedComponent = null
}
componentWillUnmount() {
if (this.wrappedComponent) {
this.wrappedComponent.removeEventListener('mouseenter', this.handleMouseEnter)
this.wrappedComponent.removeEventListener('mouseleave', this.handleMouseLeave)
}
}
handleMouseEnter() {
this.setState({componentIsHovered: true})
}
handleMouseLeave() {
this.setState({componentIsHovered: false})
}
bindListeners(wrappedComponentInstance) {
console.log('wrappedComponentInstance', wrappedComponentInstance)
if (!wrappedComponentInstance) {
return
}
this.wrappedComponent = ReactDOM.findDOMNode(wrappedComponentInstance)
this.wrappedComponent.addEventListener('mouseenter', this.handleMouseEnter)
this.wrappedComponent.addEventListener('mouseleave', this.handleMouseLeave)
}
render() {
const props = Object.assign({}, this.props, {ref: this.bindListeners})
return (
<WrappedComponent
componentIsHovered={this.state.componentIsHovered}
{...props}
/>
)
}
}
}
The problem is that this only seems to work when WrappedComponent is a class component — with functional components the ref is always null. I would just as soon place the WrappedComponent inside <div></div> tags in my HOC and carry out the event detection on that div wrapper, but the problem is that even plain div tags will style the WrappedComponent as a block element, which doesn’t work in my use case where the HOC should work on inline elements, too. Any suggestions are appreciated!
You can pass the css selector and the specific styles you need to the Higher Order Component like this:
import React, {Component} from 'react';
const Hoverable = (WrappedComponent, wrapperClass = '', hoveredStyle=
{}, unhoveredStyle={}) => {
class HoverableComponent extends Component {
constructor(props) {
super(props);
this.state = {
hovered: false,
}
}
onMouseEnter = () => {
this.setState({hovered: true});
};
onMouseLeave = () => {
this.setState({hovered: false});
};
render() {
return(
<div
className={wrapperClass}
onMouseEnter= { this.onMouseEnter }
onMouseLeave= { this.onMouseLeave }
>
<WrappedComponent
{...this.props}
hovered={this.state.hovered}
/>
</div>
);
}
}
return HoverableComponent;
};
export default Hoverable;
And use Fragment instead of div to wrap your component:
class SomeComponent extends React.Component {
render() {
return(
<Fragment>
<h1>My content</h1>
</Fragment>
)
}
And then wrap it like this
const HoverableSomeComponent = Hoverable(SomeComponent, 'css-selector');

Resources