I am trying to build an app that uses drag-and-drop behaviour, and the component being dragged needs to be cloned elsewhere in the DOM. Since the component is already mounted, trying to mount it again causes the browser to hang.
Trying to use cloneWithProps results in a Cannot read property 'defaultProps' of undefined error.
Here's a testcase:
var TestCase = React.createClass({
getInitialState () {
return {
draggingItem: null
}
},
render () {
return <div>
<ExampleComponent onClick={this.setDraggingItem} />
{this.state.draggingItem}
</div>
},
setDraggingItem (component) {
// This gives `Cannot read property 'defaultProps' of undefined`
//React.addons.cloneWithProps(component)
// This crashes the browser
//this.setState({ draggingItem: component })
}
})
var ExampleComponent = React.createClass({
render () {
return <div onClick={this.handleOnClick}>Hello World</div>
},
handleOnClick (event) {
this.props.onClick(this)
}
})
React.render(<TestCase />, document.body)
Of course I could simply clone component.getDOMNode() in setDraggingItem, but it really seems like rendering the component or calling cloneWithProps should work?
The two things you need to create an element is: the component class (e.g. ExampleComponent) and its props. cloneWithProps is only to be used in render and only with an element coming from props which was created in another component's render. You shouldn't save elements, or pass them around other than to other components in render. Instead, you pass around objects (props) and component classes.
Since you need to know the props and component class to render it in the first place, you can handle all of this in TestCase.
var TestCase = React.createClass({
getInitialState () {
return {
draggingItem: null,
draggingItemProps: null
}
},
render () {
return <div>
<ExampleComponent onClick={this.setDraggingItem.bind(null,
/* the component class */ ExampleComponent,
/* the props to render it with */ null
)} />
{
this.state.draggingItem && React.createElement(
this.state.draggingItem,
this.state.draggingItemProps
)
}
</div>
},
setDraggingItem (component, props, event) {
this.setState({ draggingItem: component, draggingItemProps: props })
}
});
var ExampleComponent = React.createClass({
render () {
return <div onClick={this.handleOnClick}>Hello World</div>
},
// just defer the event
handleOnClick (event) {
this.props.onClick(event)
}
});
If you wish to make these valid outside this TestCase component, ensure there aren't any functions bound to TestCase in the props. Also ensure there's no children prop with react elements in it. If children are relevant, provide the {componentClass,props} structure needed to recreate them.
It's hard to tell what your actual requirements are, but hopefully this is enough to get you started.
You need be sure you're creating a new component with the same props, not mount the same one multiple times. First, setup a function that returns an instantiated components (easier to drop JSX here):
function getComponent(props) {
return ExampleComponent(props);
}
Then in your TestCase render:
return (<div>
{ getComponent({ onClick: this.setDraggingItem }) }
{ this.state.draggingItem }
</div>);
That will create the first component. Then to create a clone:
setDraggingItem(component) {
var clone = getComponent(component.props);
}
This deals with the cloning part. You still have some dragging and rendering to figure out.
Related
So I have a mounted React application that is rendered like this (it's via another framework on top):
const componentRender = (entryComponent, initialProps, targetNode) => {
root.render(wrapper);
}
On top level, I call a 3rd party library and wrap my React application inside of it.
What's the best way to integrate React inside of a 3rd party library that its supposed to render into?
The best practice is to update props within a component, not outside of the render function. Otherwise, as you've said, it will cause a full remount.
So the solution would be to call your Ext.extend function from within the App component.
function App() {
const [ready, setReady] = useState(false);
const [props, setProps] = useState({});
// Calls once on first mount.
useEffect(() => {
Ext.extend(Ext.Container, {
listeners: {
afterrender: function () {
setProps(this.myProps);
setReady(true);
},
},
constructor: function (...args) {
Object.assign(this, args);
},
onServerSuccess: function(data) {
// here is where I receive new data
setProps(data);
}
});
}, [])
return (
ready ? <Component {...props} /> : null
);
}
ReactDOM.render(App, rootNode);
If there is more interaction between Ext and the react component, for example you need to pass this.root into a function, you can still do that with the normal DOM APIs like document.getElementById. Or you can add it like this:
// use this reference to do stuff with the react object
this.root = <Component {...props} />
return this.root;
but changing the react component object directly would be an anti-pattern. All state/props updates should happen using APIs provided by the react library.
Also if for some reason you need to render the react component directly inside the Ext.Container node, then you should use ReactDOM.createPortal instead of render.
Here's how that would look.
import { createPortal } from 'react-dom';
const Portal = memo(({ children, domNode }) => createPortal(
children,
domNode,
));
function App() {
const [ready, setReady] = useState(false);
const [props, setProps] = useState({});
// Calls once on first mount.
useEffect(() => {
Ext.extend(Ext.Container, {
listeners: {
afterrender: function () {
setProps(this.myProps);
setReady(true);
},
},
constructor: function (...args) {
Object.assign(this, args);
},
onServerSuccess: function(data) {
// here is where I receive new data
setProps(data);
}
});
}, [])
return (
ready ?
<Portal domNode={extComponentNode}>
<Component {...props} />
</Portal> : null
);
}
Where extComponentNode is the node you want it rendered in. createPortal allows the react component to share state with your react App even though it's rendered in a different place, without the issue of calling render again.
I suppose you want to re-render the component with updated states without unmounting-mouting it whenever the prop(s) passed to the component changes.
Create a useEffect inside this component to re-render the component whenever the required prop(s) changes. To do this, add the prop(s) for which you want to re-render the component in the dependency array of useEffect and pass a function that will update the states of the component.
useEffect(
<function which will update the states, causing re-rendering of the component>,
[ <all props for which you want to run the function separated by a comma> ]
)
I am very new to front-end dev & I am having some trouble getting my Enzyme unit tests using Shallow. Basically, I have something like
class MyComponent extends React.Component {
constructor(props, context) {
super(props, context);
}
render() {
const {
handleClick,
...other
} = this.props;
return (
<div className="someClass">
// a bunch of stuff
<div className="buttonArea">
<MyComponentChild onClick={handleClick} />
</div>
</div>
);
}
MyComponent.propTypes = {
handleClick: PropTypes.func.isRequired,
...other
};
export default MyComponent;
}
handleClick is a callback function defined in the container (i.e ComponentContainer) that MyComponent belongs to. I am passing it as a prop into MyComponent, and subsequently MyComponentChild (which is a button component). I want to test whether handleClick fires when MyComponentChild is clicked.
My current Enzyme test
it('handleClick should fire when clicked', () => {
const mockCallbackFn = jest.fn();
const wrapper = shallow(<MyComponent {handleClick = { mockCallbackFn }} />);
wrapper.find('MyComponentChild').simulate('click');
expect(mockCallbackFn).toHaveBeenCalled();
});
However, this test is currently failing, as mockCallbackFn is apparently never called. But, this is also passing
expect(wrapper.find('MyComponentChild').prop('handleClick')).toEqual(mockCallbackFn);
What am I doing wrong? Any help much appreciated!
simulate(someEventName) does really simple thing: it calls prop with name of "on" + someEventName. So simulate('click') runs .props().onClick().
But your component uses handleClick prop, that's why it does not called by simulate()
wrapper.find('MyComponentChild').props().handleClick();
Name simulate is so confusing that team is going to remove it out(https://github.com/airbnb/enzyme/issues/2173).
Side note: you don't need extra braces when declaring props. I mean {handleClick = { mockCallbackFn }} better be handleClick={mockCallbackFn} since it's typical for React code and looks less confusing.
You need to use mount instead of shallow. shallow only mounts the first level of components, so your ComponentChild is not being mounted and your click handler isn't being passed in.
You can see this yourself by calling debug() on your wrapper and console.log-ing it.
The React documentation says to pass the function defined in the Root component as a prop to the Child Component if you plan to update context from a nested component.
I have implemented the same:
import React from 'react';
const DataContext = React.createContext();
/**
* The App.
*/
export default class App extends React.Component {
constructor() {
super();
this.updateGreet = this.updateGreet.bind( this );
this.state = {
greet: '',
updateGreet: this.updateGreet
}
}
updateGreet() {
this.setState({
greet: 'Hello, User',
});
}
render() {
return (
<DataContext.Provider value={ this.state }>
<GreetButton />
<DisplayBox />
</DataContext.Provider>
)
}
}
/**
* Just a button element. On clicking it sets the state of `greet` variable.
*/
const GreetButton = () => {
return (
<DataContext.Consumer>
{
( { updateGreet } ) => {
return <button onClick={ updateGreet }>Greet</button>
}
}
</DataContext.Consumer>
)
}
/**
* Prints the value of `greet` variable between <h1> tags.
*/
const DisplayBox = () => {
return (
<DataContext.Consumer>
{
( { greet } ) => {
return <h1>{ greet }</h1>
}
}
</DataContext.Consumer>
)
}
It's a very simple React App I created for learning the Context API. What I'm trying to achieve is to define the updateGreet() method within the GreetButton component instead of defining it inside the App component since the function has nothing to do with the App component.
Another advantage I see is that if I choose to remove the GreetButton component altogether, then I need not keep track of all the methods it uses defined within another components.
Is there a way we can achieve this?
I would argue that the updateGreet method does have to do with App since it is manipulating App state.
I don't see this as a context-specific issue so much as the normal react practice of passing functions down to child components.
To accomplish your wish you could bind and pass the App's setState method to the provider and then implement updateGreet in the GreetButton component, but that would be an anti-pattern and I wouldn't recommend it.
When I am working with the Context API I typically define my context in a separate file and implement a custom provider to suit my needs, passing the related methods and properties down and consuming them throughout the tree as needed.
Essentially, implement what you have in App as its own Provider class GreetProvider. In the render method for GreetProvider simply pass the children through:
render() {
return (
<DataContext.Provider value={ this.state }>
{ this.props.children }
</DataContext.Provider>
)
}
Now, all of your greeting logic can live together at the source, with the context. Use your new GreetProvider class in App and any of its children will be able to consume its methods.
Lets say I have a component defined like this -
// actioncomponent.js
import React from 'react';
class ActionComponent extends React.Component {
state = {
isAction: false;
}
doAction = () => {
this.setState({isAction: true})
}
render () {
return (
<div>
Some render stuff..
</div>
)
}
}
export default ActionComponent
From another completely different file I want to set the state for the first component without rendering it in the new file so I need not use refs or props.
// newfile.js
import ActionComponent from './actioncomponent.js'
ActionComponent.doAction()
I'm aware the doAction can't be exported and calling it static doesn't have access to state either. How do I achieve something like this?
In React ecosystem you probably don't need this.
You can pass this method to a child component:
class ActionComponent extends React.Component {
state = {
isAction: false
}
doAction = () => {
this.setState({isAction: true})
}
render () {
return (
<div>
<Child doAction={this.doAction} />
</div>
)
}
}
And then in a Child component you can fire this action
// ...
render() {
<button onClick={() => props.doAction()}>Test</button>
}
If you need to fire action on parent, instead of child you might want to structure your state on upper level, or lift state up.
You can also achieve similar goal without drilling props, but you'll need some state management tool, e.g. Redux or in some cases Context API would be a great fit.
I'd like to know, how to handle the PropTypes Error when passing a component as a child:
Failed prop type: The prop `value` is marked as required in `ChildComponent`, but its value is `undefined`.
The render works as expected and it's passing the value prop correctly.
I suppose this happens because I am putting the component in the App component's render function without any props.
I am only passing those props to the ChildComponent when the ParentComponent maps over its children (which is the ChildComponent).
See the code: https://codesandbox.io/embed/r70r5z3j9q
Is there a way to prevent this from happening?
How should I be structuring my components?
Am I not supposed to passed components as children?
EDITED: Changed prop "name" to "value". To give it a more generic feel.
I tried to simplify the problem in the code.
I know I could pass the prop directly in App.
The use case would be when the parent is doing calculations and those calculations are supposed to be passed to the child. Without explicitly knowing what these children are.
That's why I'm using it as child in the first place.
You're using cloneElement and you're passing prop to it, not to original element. To fix it, pass props directly:
const App = () => (
<div>
<ParentComponent>
<ChildComponent name="bob" />
</ParentComponent>
</div>
);
You could easily pass component as a prop (not children) to you ParentComponent and render it only after it takes some heavy calculations:
const App = () => (
<div>
<ParentComponent component={ChildrenComponent} />
</div>
);
const ParentComponent extends React.Component {
state = { heavyComputationFinished: false } // initial state
componentDidMount() {
runYourHeavyComputations
.then(() => { this.setState({ heavyComputationsFinished: true }) })
}
render() {
const { component } = this.props
const { heavyComputationsFinished, name } = this.state
// return nothing if heavy computations hasn't been finished
if (!heavyComputationsFinished) { return null }
// we're getting this component (not its rendering call) as a prop
return React.render(component, { name })
}
}