React: Cannot assign to read only property '_status' of object '#<Object>' - javascript

I've been struggling over this error for a while now. It happens when I try to open react-bootstrap Modal with dynamically passed lazy component referrence and props to render it inside. It worked with classic import.
First row points to some react's internal lazy handler:
This is how modals are handled inside my ModalProvider:
const ModalList = React.memo(({ modalList, closeModalByIndex, confirmModalExitByIndex }) =>
modalList.map((modalDef, index) => {
const closeModal = () => closeModalByIndex(index);
const onConfirmExitChange = (confirmExit) => confirmModalExitByIndex(index, confirmExit);
const props = { ...modalDef, key: index, closeModal, onConfirmExitChange };
switch (modalDef.type) {
case TYPE_LIST:
return (
<React.Suspense fallback={fallback}>
<ListModal {...props} />
</React.Suspense>
);
case TYPE_FORM:
return (
<React.Suspense fallback={fallback}>
<FormModal {...props} />
</React.Suspense>
);
case TYPE_LIST_MULTI:
return (
<React.Suspense fallback={fallback}>
<ListMultiModal {...props} />
</React.Suspense>
);
default:
return null;
}
})
);
And this is how it is passed:
const openListModal = (Component, componentProps) => openModal(Component, componentProps, TYPE_LIST);
Anyone with deeper understanding what could possibly cause this?

Found out by trial by error. It was caused by immer's produce function which builds read-only deep copy of object.
setModalList(
produce(modalList, (modalList) => {
modalList.push({ Component, componentProps, type, show: true });
})
);

Related

How to add class to a passed Component

When trying to pass a component as a prop of another component, everything works fine.
But if i want instead pass a Component and handle its css classes inside the children, I'm currently lost.
In my mind im trying to achieve something similar to this:
import Navbar from 'what/ever/path/Navbar/is/in/Navbar.js';
export default function ParentComponent {
return(
<Navbar NavIcon={<MyIcon/>} />
)
}
.... Imports etc...
export default function Navbar(props) {
const {NavIcon} = props;
return(
<Navigation>
// Now use the Prop as a Component and pass default classNames to it.
// So that we don't need to wrap everything inside a span / div etc.
<NavIcon className="AddCustomStylesAlwaysHere" />
</Navigation>
)
}
Two approaches come to my mind:
Passing a component
Just pass the component and let the parent take care of its instantiation. This way, the only changes you need is making sure <MyIcon /> accepts a className prop:
const MyIcon = ({ className }) => {
return <div className={className} />
};
const Navbar = ({ NavIcon }) => {
return (
<Navigation>
<NavIcon className="AddCustomStylesAlwaysHere" />
</Navigation>
);
};
<Navbar NavIcon={MyIcon} />
Passing an element instance
This way, you take care of instantiating the component and the parent just renders it. In this case, you have to use React utilities to modify existing elements (https://reactjs.org/docs/react-api.html#cloneelement):
const MyIcon = ({ className }) => {
return <div className={className} />
};
const Navbar = ({ NavIcon }) => {
return (
<Navigation>
{React.cloneElement(NavIcon, { className: 'AddCustomStylesAlwaysHere' })}
</Navigation>
);
};
<Navbar NavIcon={<MyIcon />} />
You can use React.Children.map in combination with React.cloneElement:
{
React.Children.map(children, ( child, idx ) => {
return React.cloneElement(child, { className: 'additional-classnames' })
})
}

Cannot render component images in component when using or mapping on state variable

I am having trouble understanding why I cannot get images to show up in my components. I have a boolean which indicates loading, and an array that gets filled async. When I finish, I set the boolean and the component re renders. Now, I want to create a card for each item in the array and put in in a card deck (this is from react-bootstrap if that wasn't obvious). I can do this with any given boolean and array, but not with the boolean and arrays created with React.useState... Why is that and how should I go about fixing this?
I encountered this problem quite a few hours ago, and have tracked down its source to this minimal working example that still reflects what I am trying to do, but I am unsure of what to do from here.
function TestCard() {
return (
<Card>
<Card.Img src="holder.js/200x200" />
</Card>
);
}
I am trying to render the following component:
function MainComponent() {
const [boolState, setBoolState] = React.useState(false);
const [arrayState, setArrayState] = React.useState([]);
React.useEffect(() => {
setTimeout(() => {
setBoolState(true);
setArrayState([1,2,3]);
}, 2000);
});
return (
<>
{/* This works */}
{
true &&
<CardDeck>
{
[1,2,3].map(_ => {
return (
<TestCard />
);
})
}
</CardDeck>
}
{/* This doesn't, why? */}
{
boolState &&
<CardDeck>
{
arrayState.map(_ => {
return (
<TestCard />
);
})
}
</CardDeck>
}
</>
);
}
Code sandbox

How can I test a switch case that depends on a parameter?

I have this:
const renderComponents = () => {
switch (selectedService) {
case 'otherservices':
return <SoftLayerCancellationRequests />;
case 'dedicatedhosts':
return <GetDedicatedHostsCancellations />;
case 'virtualguestsservers':
return <SoftLayerGetVirtualGuests />;
case 'baremetalservers':
return <GetBareMetalServersCancellations />;
default:
return null;
}
};
Which at the end is called on the return statement of the component:
return (
<>
<Header pageTitle={t('cancellations.header')} />
{accountId ? (
<>
<TableToolbarComp />
{renderComponents()}
</>
) : (
<UpgradeMessage />
)}
</>
);
And the selectedService parameter is coming from a store:
export default compose(
connect(store => ({
accountId: store.global.softlayerAccountId,
selectedService: store.cancellations.selectedService,
})),
translate(),
hot(module),
)(Cancellations);
What can I do to test that switch case?
The function under renderComponents should accept selectedService as a parameter:
const renderComponents = (selectedService) => {
switch (selectedService) {
// ...
}
};
By not relying on a closure, the function becomes pure and is way easier to unit test :
it('renders a SoftLayerCancellationRequests when passed "otherservices" as parameter', () => {
const wrapper = shallow(renderComponents('otherservices'));
expect(wrapper.find(SoftLayerCancellationRequests)).toHaveLength(1);
})
Yet, I see little value in such tests. This is because the function basically acts as a simple map :
const serviceToComponent : {
otherservices: SoftLayerCancellationRequests,
dedicatedhosts: GetDedicatedHostsCancellations,
virtualguestsservers: SoftLayerGetVirtualGuests,
baremetalservers: GetBareMetalServersCancellations
}
which seems a bit dull to test.
A more meaningful test would be to test the behaviors of the component that uses such a mapping.

How to pass extra props down to children of arry.map

I have been trying to pass extra props own to the children being created with the .map function but I have not been able to succeed in passing them succesfully.
This is my code:
export const CommentsListShanghai = (props) => {
const newTimestamp = props.timestamp;
console.log(newTimestamp);
const comments = props.comments;
if (comments.length > 0 ) {
return (
<ButtonToolbar className="comment-list">
{comments.map((com) => {
return (
com.adminSpark ?
<CommentsModal
className="comments-modal"
data-comments-modal={props.newTimestamp}
key={ com._id }
comment={ com }
city={com.city}
person={com.person}
location={com.location}
title={com.title}
content={com.content}
fileLink={com.fileLink}
timestamp={com.timestamp}
createdBy={com.createdBy}
/> :
<CommentsModal
key={ com._id }
comment={ com }
city={com.city}
person={com.person}
location={com.location}
title={com.title}
content={com.content}
fileLink={com.fileLink}
timestamp={com.timestamp}
createdBy={com.createdBy} />
)
})}
</ButtonToolbar>
);
} else {
return (
<Alert bsStyle="warning">No sparks yet. Please add some!</Alert>
);
}
};
CommentsListShanghai.propTypes = {
comments: React.PropTypes.array,
};
I am able to pass all the props of the comments const that I created, the problem is that besides these props I also need to pass an extra prop which is available in the CommentsListShanghai. How am I able to pass an extra props to this array?
I am able to console.log(newTimestamp) without a problem but don't understand how I can pass it down to the .map function.
Instead of
data-comments-modal={props.newTimestamp}
just use
data-comments-modal={props.timestamp}
The props here is still referring to the context of CommentsListShanghai.

React Transferring Props except one

React suggests to Transfer Props. Neat!
How can I transfer all of the props but one?
render: function(){
return (<Cpnt {...this.propsButOne}><Subcpnt one={this.props.one} /></Cpnt>);
}
You can use the following technique to consume some of the props and pass on the rest:
render() {
var {one, ...other} = this.props;
return (
<Cpnt {...other}>
<Subcpnt one={one} />
</Cpnt>
);
}
Source
What you need to do is to create a copy of the props object and delete the key(s) you don't want.
The easiest would be to use omit from lodash but you could also write a bit of code for this (create a new object that has all the keys of props except for one).
With omit (a few options at the top, depending on what package you import/ES flavor you use):
const omit = require('lodash.omit');
//const omit = require('lodash/omit');
//import { omit } from 'lodash';
...
render() {
const newProps = omit(this.props, 'one');
return <Cpnt {...newProps}><Subcpnt one={this.props.one} /></Cpnt>;
}
If you have a lot of props you don't want in ...rest e.g. defaultProps, it can be annoying to write all of them twice. Instead you can create it yourself with a simple loop over the current props like that:
let rest = {};
Object.keys(this.props).forEach((key, index) => {
if(!(key in MyComponent.defaultProps))
rest[key] = this.props[key];
});
Thank you #villeaka!
Here's an example of how I used your solution for other people to better understand it's usage.
I basically used it to create a stateless wrapping-component that I then needed to pass its props to the inner component (Card).
I needed the wrapper because of the rendering logic inside another top level component that used this wrapper like this:
<TopLevelComponent>
{/* if condition render this: */}
<CardWrapper {...props}> {/* note: props here is TLC's props */}
<Card {..propsExceptChildren}>
{props.children}
</Card>
</CardWrapper>
{/* if other condition render this: */}
{/* ... */}
{/* and repeat */}
</TopLevelComponent>
where several conditions determine what comes after the H4 in the wrapper (see actual rendered node tree below).
So basically, I didn't want to duplicate code by writing the entire part that comes before {children} in the example below, for each arm of the conditional in the top level component that renders multiple variants of the wrapper from above example:
const CardWrapper: React.FC<IRecentRequestsCardProps> = (props) => {
const { children, ...otherProps } = props;
return (
<Card {...otherProps} interactive={false} elevation={Elevation.ONE}>
<H4>
Unanswered requests
</H4>
{children}
</Card>
);
};
And concrete usage in a React render function:
if (error)
return (
<CardWrapper {...props}>
<SimpleAlert title="Eroare" intent={Intent.DANGER}>
{error}
</SimpleAlert>
</CardWrapper>
);
if (loading)
return (
<CardWrapper {...props}>
<NonIdealState
icon="download"
title="Vă rog așteptați!"
description="Se încarcă cererile pentru articole..."
/>
</CardWrapper>
);
if (!data)
return (
<CardWrapper {...props}>
<NonIdealState
icon="warning-sign"
title="Felicitări!"
description="Nu există cereri fără răspuns."
/>
</CardWrapper>
);
// etc.
So the above just adds the H4 header before the children of the wrapper and also passes down the props that it has been passed down to, to the inner Card component.
The simplest way I found so far:
const obj = {
a: '1',
b: '2',
c: '3'
}
const _obj = {
...obj,
b: undefined
}
This will result in _obj having all the props except b
Try this:
function removeProps(obj, propsToRemove) {
let newObj = {};
Object.keys(obj).forEach(key => {
if (propsToRemove.indexOf(key) === -1)
newObj[key] = obj[key]
})
return newObj;
}
const obj = {nome: 'joao', tel: '123', cidade: 'goiania'}
const restObject = removeProps(obj, ['cidade', 'tel'])
console.log('restObject',restObject)
restObject
{
nome:"joao"
}
I had this issue when extending Material UI. A component would emit a warning if an unknown property was passed at all. I solved it slightly differently by specifically deleting the properties I didn't want to pass:
const passableProps = { ...props } as Partial<typeof props>;
delete passableProps.customValidity;
return (
<TextField { ...passableProps } // ...
);

Categories