In a several components i have a function returning URL of user avatar:
import defaultAvatar from 'assets/images/default-pic.jpg'
class MyComponent extends Component {
userAvatar () {
const { profile } = this.props
if (profile.has_avatar) {
return profile.avatar
} else {
return defaultAvatar
}
}
}
is there a way to DRY this function between multiple components?
With Default Props
If you make an avatar component that accepts avatar as a top level property, then you can just use default props to specify the value when it's not provided.
function Avatar({ avatar }) {
return <img src={avatar} />;
}
Avatar.defaultProps = { avatar: defaultAvatar };
Then render this new component from inside your existing one.
return (
<Avatar profile={props.profile} />
);
This way you can keep everything declarative and remove the need for a has_avatar property.
As a Utility Function
But you could also just rip it straight out and fiddle the arguments so you can call it from anywhere.
function getUserAvatar(profile) {
if (profile.has_avatar) {
return profile.avatar
} else {
return defaultAvatar
}
}
Then rewrite your original code.
class MyComponent extends Component {
userAvatar () {
const { profile } = this.props
return getUserAvatar(profile);
}
}
As a Higher Order Component
It would also be possible to implement this as a higher order component.
function WithAvatar(Component) {
return function(props) {
const { profile } = props;
const avatar = getUserAvatar(profile);
return <Component avatar={avatar} {...props} />;
};
}
This would allow you to wrap any existing component with the WithAvatar component.
function Profile(props) {
const { profile, avatar } = props;
return (
<div>
<img src={avatar.src} />
<span>{profile.name}</span>
</div>
);
}
const ProfileWithAvatar = WithAvatar(Profile);
render(
<ProfileWithAvatar profile={exampleProfile} />,
document.getElementById('app')
);
Passing profile as a prop to the outer component causes WithAvatar to process it and select the correct avatar, then pass it down as a prop to the wrapped component.
If you've used the React.createClass approach you could able to use mixins to share code across components. Since you are using ES6 approach you can checkout HOCs (Higher Order Components)
Reference: https://gist.github.com/sebmarkbage/ef0bf1f338a7182b6775
Related
I am still learning React and I am wondering if it is possible to make the function component below into a class component.
Component:
import React, { useState } from "react"
import { useStaticQuery, graphql } from "gatsby"
import Navbar from "./navbar"
const Layout = ({ location, title, children }) => {
const rootPath = `${__PATH_PREFIX__}/`
const [classNames, setClassNames] = useState('')
const updateClasses = (classNames) => {
setClassNames(classNames)
}
const data = useStaticQuery(graphql`
{
site {
siteMetadata {
menuLinks {
link
name
}
}
}
}
`)
return (
<div>
<Navbar pages={ data.site.siteMetadata.menuLinks } updateClassNames={updateClasses} />
<main className={classNames}>{children}</main>
</div>
)
}
export default Layout
My biggest issue is with the parameters that are passed to the function location, title, children. What will happen with them. I am not using them at the moment, but will need them later.
Class or function component is not much different except using hooks.
With your current function component, just use the location, title, etc props like other variables in a normal function.
Why do you need to convert into class component?
You don't need a class component to lift up a class, value, or any data in a child component, it's the same behavior rather than class-based component or functional component. You just need to pass via props a function that it will be triggered in a child component to lift up some data again to the parent.
In your parent component, you need to set the function. Without knowing its structure, it would look like:
someFunction= value => {
console.log('I have the value: ', value)
}
return <Layout someFunction={someFunction}
Disclaimer: you may need to adapt the code to your component. The idea is to set a function and pass it via props in the return.
Then, in your <Layout> component, you can destructure the function as you do with location, title and children, and trigger when you need it:
import React, { useState } from "react"
import { useStaticQuery, graphql } from "gatsby"
import Navbar from "./navbar"
const Layout = ({ location, title, children, someFunction }) => {
const rootPath = `${__PATH_PREFIX__}/`
const [classNames, setClassNames] = useState('')
const updateClasses = (classNames) => {
setClassNames(classNames)
}
const handleClick=()=>{
someFunction('hello')
}
const data = useStaticQuery(graphql`
{
site {
siteMetadata {
menuLinks {
link
name
}
}
}
}
`)
return (
<div>
<Navbar pages={ data.site.siteMetadata.menuLinks } updateClassNames={updateClasses} onClick={()=>handleClick} />
<main className={classNames}>{children}</main>
</div>
)
}
export default Layout
In this dummy example, you will be passing 'hello' to the parent component when the <Navbar> is clicked, of course, you can pass any desired value or use a useEffect hook or whatever you need. This is the way to pass data from child to parent component.
I'm wondering if there's a pattern that allows me to prevent an HOC from recalculating based on some condition, here's an example:
const DumbComponent = () => <button>Click me<button/>;
// Third party HOC, that I can't modify
const SmartComponent = Component => class extends Component {
componentWillReceiveProps() {
// Complex stuff that only depends on one or 2 props
this.state.math = doExpensiveMath(props);
}
render() {
return <Component {...this.props} math={this.state.math} />;
}
}
const FinalComponent = SmartComponent(DumbComponent);
What I'm looking for, is a pattern that prevents this HOC from doing its thing if the props I know it depends on haven't changed. But that doesn't stop the entire tree from rerendering based on props like shouldComponentUpdate would do.
The catch is, that this HOC comes from another library and ideally I don't want to fork it.
What i got is you want to prevent hoc to recompute certain logic for that you can do as below :-
const DumbComponent = () => <button>Click me<button/>;
// Third party HOC
const SmartComponent = Component => class extends Component {
componentWillReceiveProps() {
// Complex stuff that only depends on one or 2 props
this.state.math = doExpensiveMath(props);
}
render() {
return <Component {...this.props} math={this.state.math} />;
}
}
const withHoc = SmartComponent(DumbComponent);
class FinalComponent extends Component {
render() {
if (your condition here) {
return (
<withHoc {...this.props} />
);
}
else {
return <DumbComponent {...this.props}/>;
}
}
}
export default FinalComponent;
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.
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)
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>
)
}
}