How to set defaultProp value based on value of other prop? - reactjs

I have a component where the default value of one prop depends on the value of another prop (default or user provided). We can't do the following because we don't have access to this:
static defaultProps = {
delay: this.props.trigger === 'hover' ? 100 : 0,
trigger: 'hover'
};
How can I best do this?

I'd rather suggest you to:
store that variable as an instance variable in the component's class
evaluate if it is a state variable rather than a prop (or instance)
By the way in both cases you should check when new props arrive to the component and update it if needed.
I'd go for the state variable and write something like this:
class MyComponent extends React.Component {
static propTypes = {
trigger: PropTypes.string,
}
static defaultProps = {
trigger: 'hover',
}
constructor(props) {
super(props);
this.state = {
delay: this.computeDelay(),
}
}
componentWillReceiveProps(nextProps) {
const { trigger: oldTrigger } = this.props;
const { trigger } = nextProps;
if (trigger !== oldTrigger) {
this.setState({
delay: this.computeDelay(),
})
}
}
computeDelay() {
const { trigger } = this.props;
return trigger === 'hover' ? 100 : 0;
}
render() {
...
}
}
In this way you can use this.state.delay in the render method without taking care of determining its value.

You can do it inside the render method.
render() {
const delay = this.props.trigger === 'hover' ? 100 : 0;
// Your other props
return (
<SomeComponent delay={delay} />
// or
<div>
{/*...something else, use your delay */}
</div>
);
}

Using a function component you can do it like this:
function MyComponent({
trigger,
delay: trigger === 'hover' ? 100 : 0,
}) {
return <div>...</div>
}
MyComponent.propTypes = {
trigger: PropTypes.string.isRequired,
delay: PropTypes.number.isRequired,
};

I was facing a similar issue and I found a method based solution missing in this discussion therefore i am writing this answer
There are two cases when you might want to pass default props
Case 1: when you want to choose defaultProps based on a Static value
Case 2: when you want to choose defaultProps based on a Method
Solution for Case 1
class Shape extends Component{
static defaultProps = {
colour: 'red',
}
render(){
const {colour} = this.props;
// Colour will always be 'red' if the parent does not pass it as a prop
return <p>{colour}</p>;
}
}
solution for Case 2
class Shape extends Component{
calcArea = () => {
console.log("Area is x");
}
render(){
const {calcArea} = this.props;
// calcArea will be evaluated from props then from the class method
return <button onClick={calcArea || this.caclArea}></button>;
}
}

Related

Calling props from a container

I am a little confused on the idea of using props in the context I am using for my React app. In my component, I need to check if the value of a certain prop (props.companyCode) matches a certain string, and only then will it print out a <p> of what I need. Below is what I have for calling the prop in the component:
Components/CompanyContact.jsx
class CompanyContact extends React.Component {
help() {
if (this.props.companyInfoList.companyCode === '1234') {
return <p>something</p>;
}
return <p>somethingelse</p>;
}
render() {
const help = this.help();
return (
<div>
{help};
</div>
)}}
export default CompanyContact;
And this is what I have for the container:
Container/InfoContainer.jsx
class InfoContainer extends React.Component {
constructor(props) {
super(props);
this.state = {
companyInfoList: null,
};
}
async componentWillMount() {
const companyInfoCachedData = CachingService.getData('companyInfoList');
if (companyInfoCachedData) {
this.setState({ companyInfoList: companyInfoCachedData });
return;
}
}
async getCompanyInfo(accessToken) {
try {
const companyProfileResponse = await requestAWSGet('api/company-profile', undefined, accessToken);
CachingService.setData('companyInfoList', companyProfileResponse);
this.setState({ companyInfoList: companyProfileResponse });
} catch (err) {
throw err;
}
}
render() {
return (
<CompanyContact companyInfoList={this.state.companyInfoList} />
);
}
}
export default InfoContainer;
Nothing is returned when I run the application and I believe it's because I'm not calling the prop correctly in my component but I am unsure as to how to go about fixing it. I'm fairly new to working with props so still trying to get my bearings.
I'm assuming you are getting an error somewhere because of this not having props and this.props.companyInfoList.companyCode trying to access a property on a non object. this.props.companyInfoList is initially set to null so accessing a property on it will break.
A few strategies to fix the problem:
Default it to an empty object
this.state = {
companyInfoList: {},
}
Block the rendering of the component until it has a value:
if (this.state.companyInfoList) {
return (
<CompanyContact companyInfoList={this.state.companyInfoList} />
);
} else {
return null;
}
Check that the prop is an object and has the key companyCode on it:
if (this.props.companyInfoList &&
this.props.companyInfoList.companyCode &&
this.props.companyInfoList.companyCode === '1234') {
In addition, this will be in the wrong context and the changes above will most likely no be enough. Try changing to an arrow function like this:
help = () => {
// your code here
}
I would personally refactor that component logic and directly use the prop value inside the render method like:
class CompanyContact extends React.Component {
render() {
const { companyInfoList } = this.props;
return companyInfoList && companyInfoList.companyCode === '1234' ? (
<p>something</p>
) : (
<p>somethingelse</p>
)
}
}
export default CompanyContact;

How to get the DOM node from a Class Component ref with the React.createRef() API

I have these two components:
import { findDOMNode } from 'react-dom';
class Items extends Component {
constructor(props) {
super(props);
this.ref = React.createRef();
this.selectedItemRef = React.createRef();
}
componentDidMount() {
if (this.props.selectedItem) {
this.scrollToItem();
}
}
componentWillReceiveProps(nextProps) {
if (this.props.selectedItem !== nextProps.selectedItem) {
this.scrollToItem();
}
}
scrollToItem() {
const itemsRef = this.ref.current;
const itemRef = findDOMNode(this.selectedItemRef.current);
// Do scroll stuff here
}
render() {
return (
<div ref={this.ref}>
{this.props.items.map((item, index) => {
const itemProps = {
onClick: () => this.props.setSelectedItem(item.id)
};
if (item.id === this.props.selectedItem) {
itemProps.ref = this.selectedItemRef;
}
return <Item {...itemProps} />;
})}
</div>
);
}
}
Items.propTypes = {
items: PropTypes.array,
selectedItem: PropTypes.number,
setSelectedItem: PropTypes.func
};
and
class Item extends Component {
render() {
return (
<div onClick={() => this.props.onClick()}>item</div>
);
}
}
Item.propTypes = {
onClick: PropTypes.func
};
What is the proper way to get the DOM node of this.selectedItemRef in Items::scrollToItem()?
The React docs discourage the use of findDOMNode(), but is there any other way? Should I create the ref in Item instead? If so, how do I access the ref in Items::componentDidMount()?
Thanks
I think what you want is current e.g. this.selectedItemRef.current
It's documented on an example on this page:
https://reactjs.org/docs/refs-and-the-dom.html
And just to be safe I also tried it out on a js fiddle and it works as expected! https://jsfiddle.net/n5u2wwjg/195724/
If you want to get the DOM node for a React Component I think the preferred way of dealing with this is to get the child component to do the heavy lifting. So if you want to call focus on an input inside a component, for example, you’d get the component to set up the ref and call the method on the component, eg
this.myComponentRef.focusInput()
and then the componentRef would have a method called focusInput that then calls focus on the input.
If you don't want to do this then you can hack around using findDOMNode and I suppose that's why it's discouraged!
(Edited because I realized after answering you already knew about current and wanted to know about react components. Super sorry about that!)

How to initialize the state of each item component in the component array in React?

const products = [
{
id: 1,
...
},
{
id: 2,
...
},
{
id: 3,
...
},
];
I created the ProductList component, which contains 3 Product components:
class ProductList extends Component {
constructor(props) {
super(props)
}
render() {
const productComponents = products.map((product) => (
<Product
key = {'product-' + product.id}
id = {product.id}
...
/>
));
return (
<ul className="holder-list row">
{productComponents}
</ul>
);
}
}
class Product extends Component {
constructor(props) {
super(props)
}
render() {
return(..)
}
}
How and in which component in the constructor to set a different initial state for all three products?
I want set the initial value of this.state for each Product different.
Example:
for Product with id:1 - this.state={color: blue},
for Product with id:2 - this.state={color: yellow},
for Product with id:3 - this.state={color: red}.
How can I do something like this?
This is how you could approach setting state color for Product. These are both inspired from a great article You Probably Don't Need Derived State which provides some great examples on how to handle "derived state".
ProductList - Create a method the returns a string color value based on your id to color requirements. This can be outside of the class definition, it doesn't/shouldn't need to be a method on class ProductList as it doesn't need the this or similar. Add an additional prop, something like defaultColor, that is passed to each instance of Product:
const getColor = id => {
switch (id) {
case 1:
return 'blue';
case 2:
return 'yellow';
case 3:
return 'red'
default:
return '';
}
};
// ...
render() {
const productComponents = products.map((product) => (
<Product
key = {'product-' + product.id}
id = {product.id}
defaultColor={getColor(product.id)}
...
/>
));
}
Product - Set initial state using the defaultColor prop being passed in. Using a different property would allow each Product component to fully control it's own color state value/changes with something like an <input />, but copy over the initial color value:
class Product extends Component {
state = { color: this.props.defaultColor };
// ...
render() {
return ({/* ... */});
}
}
Here is a StackBlitz demonstrating the functionality in action.
The other options is using static getDerivedStateFromProps() in Product. It conditionally checks if the id prop has changed to avoid setting state unnecessarily and overriding Product local state values. We are keeping track of the previous id value so that it can be used in the conditional statement to see if any changes actually happened:
class Product extends Component {
constructor(props) {
super(props);
this.state = {
prevId: -1,
color: ''
};
}
static getDerivedStateFromProps(props, state) {
if (props.id !== state.prevId) {
switch (props.id) {
case 1:
return { color: 'blue', prevId: props.id };
case 2:
return { color: 'yellow', prevId: props.id };
case 3:
return { color: 'red', prevId: props.id };
default:
return null;
}
}
return null
}
render() {
return ({/* ... */});
}
}
Here is a StackBlitz demonstrating this functionality.
It's hard to say exactly how to approach this as it may be likely you do not need state in Product. That Product can act as a "dumb" component just receiving props and emitting value changes to a higher order component like ProductList.
Hopefully that helps!

Passing function from props to outside of the component (High Order Component)

I'm trying to pass the function from high order component outside the class because I need to call it but it is also needed to be pass back. Hard to explain, here's the code:
Wrapped Component:
class foo extends React.Component {
....
}
foo.list = [
{
name: "Home",
action: this.props.funcFromHoc //this.props.from foo class, how can i access this because it is outside the component?
}
]
export default bar(foo);
High Order Component:
export default function bar(WrappedComponent) {
funcFromHoc() {
alert("Hello from HOC function!");
}
render() {
return (
<WrappedComponent
{ ...this.props }
funcFromHoc={ this.funcFromHoc }
);
}
}
What I'm actually doing:
I have a base screen (HOC) with a 2 drawers, that has some functions that controls their behavior. I need this 2 drawers on many screens that I'll make, I don't want to put the configuration of the drawer for every screens, that's why I create a HOC for this. My problem is, the list on the drawer on HOC is dynamic on each screens, and they have specific function that I set on each screens, how can I pass a function from the screen component to HOC?
Am I missing something or this? Am I doing it wrong? Did I missed some of the proper usage of High Order Components? Or what method should I use for this? Any hint or help will be very much appreciated. Thank you.
I've found a solution, it solves my problem using Inheritance Inversion.
class foo extends React.Component {
list() {
return [
{
name: "Home",
action: this.funcFromHoc //successfully accessed it here!
}
];
}
}
export default bar(foo);
(High Order Component):
export default function bar(WrappedComponent) {
return class Bar extends WrappedComponent {
funcFromHoc() {
alert("Hello from HOC function!");
}
render() {
return (
//call super.list() here to populate drawer list
{super.render()}
);
}
}
}
It sounds like you want to be able to pass additional parameters to the HOC. You could pass a mapping function when you call the HOC, like the color variable below. Note the HOC is defined slightly differently as well in order to take additional arguments.
edit: nevermind, you won't have access to funcFromFoo unless it is a prop of foo (i.e. defined in mapDispatchToProps). heh- i tried. may need to rethink the design so this isn't a requirement.
function doSomethingWithList(props) {
const { actionName, funcFromHOC, funcFromFooProps } = props;
if (actionName === 'Home') {
funcFromHOC();
} else {
funcFromFooProps();
}
};
const makeToggleable = (WrappedComponent, color) => {
return class ToggleableComponent extends React.Component {
constructor(props) {
super(props);
this.state = { toggled: false };
this.toggleColor = this.toggleColor.bind(this);
}
toggleColor() {
this.setState({ toggled: !this.state.toggled });
}
render() {
const fontColor = this.state.toggled? color: 'black';
return (
<WrappedComponent { ...this.props }
style={{color: fontColor}}
onClick={this.toggleColor} />
);
}
}
}
export default makeToggleable(BaseComponent, 'red');
const listItems = [ 'Home', 'Other' ];
export default bar(foo, listItems, doSomethingWithList);
pass listItems and doSomethingWithList for OP's question, modify HOC to call doSomethingWithList with props
example code from https://spin.atomicobject.com/2017/03/02/higher-order-components-in-react/

React child component can't get props.object

My parent component is like this:
export default class MobileCompo extends React.Component {
constructor(props) {
super(props);
this.state = {
data: null,
datasets: {}
};
this.get_data = this.get_data.bind(this);
}
componentWillMount() {
this.get_data();
}
async get_data() {
const ret = post_api_and_return_data();
const content={};
ret.result.gsm.forEach((val, index) => {
content[val.city].push()
});
this.setState({data: ret.result.gsm, datasets: content});
}
render() {
console.log(this.state)
// I can see the value of `datasets` object
return (
<div>
<TableElement dict={d} content={this.state.data} />
<BubbleGraph maindata={this.state.datasets} labels="something"/>
</div>
)
}
}
child component:
export default class BubbleGraph extends React.Component {
constructor(props) {
super(props);
this.state = {
finalData: {datasets: []}
};
console.log(this.props);
// here I can't get this.props.maindata,it's always null,but I can get labels.It's confusing me!
}
componentWillMount() {
sortDict(this.props.maindata).forEach((val, index) => {
let tmpModel = {
label: '',
data: null
};
this.state.finalData.datasets.push(tmpModel)
});
}
render() {
return (
<div>
<h2>{this.props.labels}</h2>
<Bubble data={this.state.finalData}/>
</div>
);
}
}
I tried many times,but still don't work,I thought the reason is about await/async,but TableElement works well,also BubbleGraph can get labels.
I also tried to give a constant to datasets but the child component still can't get it.And I used this:
this.setState({ datasets: a});
BubbleGraph works.So I can't set two states at async method?
It is weird,am I missing something?
Any help would be great appreciate!
Add componentWillReceiveProps inside child componenet, and check do you get data.
componentWillReceiveProps(newProps)
{
console.log(newProps.maindata)
}
If yes, the reason is constructor methos is called only one time. On next setState on parent component,componentWillReceiveProps () method of child component receives new props. This method is not called on initial render.
Few Changes in Child component:
*As per DOC, Never mutate state variable directly by this.state.a='' or this.state.a.push(), always use setState to update the state values.
*use componentwillrecieveprops it will get called on whenever any change happen to props values, so you can avoid the asyn also, whenever you do the changes in state of parent component all the child component will get the updates values.
Use this child component:
export default class BubbleGraph extends React.Component {
constructor(props) {
super(props);
this.state = {
finalData: {datasets: []}
};
}
componentWillReceiveProps(newData) {
let data = sortDict(newData.maindata).map((val, index) => {
return {
label: '',
data: null
};
});
let finalData = JSON.parse(JSON.stringify(this.state.finalData));
finalData.datasets = finalData.datasets.concat(data);
this.setState({finalData});
}
render() {
return (
<div>
<h2>{this.props.labels}</h2>
<Bubble data={this.state.finalData}/>
</div>
);
}
}

Resources