React - Getting refs to wrapped class components - reactjs

I have a map component that contains a child sidebar component. I am trying to do a relatively simple task of scrolling to the place in the list of places in the sidebar when it's map marker is clicked on. But, because the sidebar needs to be wrapped in withRouter and connect, I'm unable to set a ref (ref) => this.sidebar = ref in the map component.
export class Map extends React.Component {
...
handleClick() {
this.sidebar.scrollToPlace(place.id);
}
render () {
return (
<MapSidebar
// unable to set ref
/>
)
}
}
and
class MapSidebar extends React.Component {
...
scrollToPlace(id) {
this.refs[id].scrollIntoView({block: 'end', behavior: 'smooth'});
}
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MapSidebar));
I know that using wrappedComponentRef could get me the contents of withRouter, but then I still have connect to deal with.
I also tried creating a custom ref on the MapSidebar instance:
<MapSidebar
getReference={(ref) => {
this.sidebar = ref;
}} />
and then in the MapSidebar class constructor, calling:
if(this.props.getReference) {
this.props.getReference(this);
}
but that resulted in an infinite loop of that component updating (although I'm not sure I understand why).
Is there a better way to get around these issues?

I suggest you avoid refs and simply pass the scroll value down:
export class Map extends React.Component {
...
handleClick() {
this.setState({scrollToPlaceId: place.id});
}
render () {
return (
<MapSidebar
// Add a new property
scrollToPlace={this.state.scrollToPlaceId}
/>
)
}
}
Then in your sidebar component, just listen to scroll changes in componentWillReceiveProps for example
class MapSidebar extends React.Component {
...
componentWillReceiveProps(nextProps) {
if (nextProps.scrollToPlace !== this.props.scrollToPlace) {
this.refs[nextProps.scrollToPlace].scrollIntoView({block: 'end', behavior: 'smooth'});
}
}
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MapSidebar));

Store a reference in both classes:
// MapSidebar render - add this to the element you want.
<div ref={r => (this.ref = r)}>
Then in Map render:
<MapSidebar ref={r => (this.sidebar = r)}>
Now after Map has mounted you have access to the ref:
this.sidebar.ref

Related

Reusable react component with Canvas doesn't render canvas with its props

I am trying to create a reusable stateful component (it shouldn't be functional component). I need to add this component in runtime so in my App i have an array of the component (CanvasComponent) in my state to render the list of component. I also generate a random size to render the size of canvas. The problem occurs when I create second canvas, Weirdly it is only render once.
I have this problem in ChartJS and since my code base is very big I decided to simplify it by a sample.
However if you uncomment CanvasComponent in the Array it works perfectly fine.
import React from 'react';
import logo from './logo.svg';
import './App.css';
import CanvasComponent from './CanvasComponent';
class App extends React.Component {
state = {
canvasList: [
// <CanvasComponent size={30}></CanvasComponent>,
// <CanvasComponent size={50}></CanvasComponent>
]
}
handleClick = () => {
const size = Math.floor(Math.random() * (100 - 50 + 1) + 50);
const newCanvas = <CanvasComponent size={size}></CanvasComponent>
this.setState({
canvasList: [newCanvas,
...this.state.canvasList]
})
}
render() {
return (
<div className="App">
<button onClick={this.handleClick}>Add canvas</button>
{ this.state.canvasList.map((item, i) => {
return <CanvasComponent {...item.props} key={i}></CanvasComponent>
})}
</div>
);
}
}
export default App;
And the component
import React from 'react'
class CanvasComponent extends React.Component {
constructor(props) {
super(props);
this.myCanvas = React.createRef();
}
componentDidMount() {
const ctx = this.myCanvas.current.getContext('2d');
ctx.fillRect(0, 0, 100, 100);
}
render() {
console.log(this.props);
return (
<div>
<p>Size should be {this.props.size}</p>
<canvas ref={this.myCanvas} width={this.props.size} height={this.props.size} />
</div>
)
}
}
export default CanvasComponent
I believe that your issue here is that you're rendering the canvas components programmatically. If something was not present when the page first loaded, then event listeners are not actively looking for it.
I'm sure there's a more elegant solution than mine, but I tend to get around this issue by writing something like.
state={ updated: false}
componentDidMount(){
this.setState({updated:true})
}
Updating the state forces a rerender, and the event listeners will know to pay attention to the relevant component.
The issue was here, I will share here in case someone have had same issue, can find it.
Instead of
this.setState({
canvasList: [newCanvas,
...this.state.canvasList]
})
You should write
this.setState({
canvasList: [...this.state.canvasList,
newCanvas]
})
I still don't know why, but it fixed the problem.

How to get a componenet instance in React

I want to know if there is possible to get the component instance as I need.
I put my new component as a children in the main state, but is no the same object in both files.
I need to reach children state in my MainComponent. Looking in google for componenet instance doesnt help, maybe I am out of focus and the name of this is different.
Here is my MainComponent:
import React, { Component } from 'react';
import AnotherComponent from './../whatever/AnotherComponent';
class MainComponent extends Component {
constructor(props) {
super(props);
this.state = {
children: [],
};
}
addChild() {
const { children } = this.state;
this.setState({
children: children.push(<AnotherComponent />)
});
}
getChildrenState(component) {
return component.state(); // this doesn't work!
}
render() {
const { children } = this.state;
return (
<div>
{(children.map(i => (<div key={this.getChildrenState(i).id}>{i}</div>))}
</div>
)
}
And This is AnotherComponent
import React, { Component } from 'react';
class AnotherComponent extends Component {
constructor(props) {
super(props);
this.state = {
id: 144,
};
}
render() {
return (
<div>
Here it is my cHild!
</div>
)
}
Putting <AnotherComponent/> to the state doesn't make sense because it's React element object that isn't associated with specific component instance.
Accessing children state in parent component breaks the encapsulation and indicates design problem.
The instance of class component should be retrieved with a ref, and doing so to access instance state is the last resort that may be needed to extend third-party components that don't provide desired functionality.
If AnotherComponent is first-party component, it should be designed accordingly, to not require state to be accessed from the outside:
render() {
return (
<div key={this.state.id}>{this.state.id}</div>
)
}
If the output needs to be more flexible, it can make use of render prop pattern:
render() {
const render = React.Children.only(children);
return (
<div key={this.state.id}>{render(this.state.id)}</div>
)
}
And used like:
<AnotherComponent>{id => <div>{id}</div>}</AnotherComponent>
If you want to access the state of the child component ( here AnotherComponent ) then you can either :
Maintain the state inside the AnotherComponent and pass the value to the parent ( here MainComponent ) on a change listener ( whenever the state changes ), or;
Maintain the state in the parent ( here MainComponent ) and pass the value to the child as prop.
Let me know if you want me to give an example implementation.

How to get the data from React Context Consumer outside the render

I am using the new React Context API and I need to get the Consumer data from the Context.Consumer variable and not using it inside the render method. Is there anyway that I can achieve this?
For examplify what I want:
console.log(Context.Consumer.value);
What I tested so far: the above example, tested Context.Consumer currentValue and other variables that Context Consumer has, tried to execute Context.Consumer() as a function and none worked.
Any ideas?
Update
As of React v16.6.0, you can use the context API like:
class App extends React.Component {
componentDidMount() {
console.log(this.context);
}
render() {
// render part here
// use context with this.context
}
}
App.contextType = CustomContext
However, the component can only access a single context. In order to use multiple context values, use the render prop pattern. More about Class.contextType.
If you are using the experimental public class fields syntax, you can use a static class field to initialize your contextType:
class MyClass extends React.Component {
static contextType = MyContext;
render() {
let value = this.context;
/* render something based on the value */
}
}
Render Prop Pattern
When what I understand from the question, to use context inside your component but outside of the render, create a HOC to wrap the component:
const WithContext = (Component) => {
return (props) => (
<CustomContext.Consumer>
{value => <Component {...props} value={value} />}
</CustomContext.Consumer>
)
}
and then use it:
class App extends React.Component {
componentDidMount() {
console.log(this.props.value);
}
render() {
// render part here
}
}
export default WithContext(App);
You can achieve this in functional components by with useContext Hook.
You just need to import the Context from the file you initialised it in. In this case, DBContext.
const contextValue = useContext(DBContext);
You can via an unsupported getter:
YourContext._currentValue
Note that it only works during render, not in an async function or other lifecycle events.
This is how it can be achieved.
class BasElement extends React.Component {
componentDidMount() {
console.log(this.props.context);
}
render() {
return null;
}
}
const Element = () => (
<Context.Consumer>
{context =>
<BaseMapElement context={context} />
}
</Context.Consumer>
)
For the #wertzguy solution to work, you need to be sure that your store is defined like this:
// store.js
import React from 'react';
let user = {};
const UserContext = React.createContext({
user,
setUser: () => null
});
export { UserContext };
Then you can do
import { UserContext } from 'store';
console.log(UserContext._currentValue.user);

Does React allow getting or setting props outside of components

When I have a component inside a component
var MySubComponent;
class MyComponent {
...
getChildProps() {
console.log(MySubComponent.props.nameToGet)
}
...
MySubComponent = withCustomAudio(props => {return(<div>...</div>)});
...
render() {
return(...<MySubComponent {...this.props}/>...)
}
...}
After component render I'd like to get or set the prop of the subcomponent. It returns cant access nameToGet of undefined
Spread attributes is not what I want
(https://zhenyong.github.io/react/docs/transferring-props.html)
No, you won't be able to get/set props like this. It's not the React way. You should read more about thinking in React.
Basically, if you want to change child props in the parent, they should be part of the parent state and passed down to the child as props.
Example:
import React, {Component} from "react";
class Parent extends Component {
state = {
childProp: "Not changed yet"
}
handleChildPropChange = () => {
this.setState({
childProp: "I changed"
})
}
render() {
return <Child myProp={this.state.childProp} onChangeChildProp={this.handleChildPropChange} />;
}
}
class Child extends Component {
render() {
return (
<div onClick={onChangeChildProp}>{this.props.myProp}</div>
)
}
}

React cloneElement and component instance

I have the following higher order component that I am trying to wrap in a container element that is supplied as a prop:
import React, { PropTypes } from 'react';
export default (Component) => {
return class extends React.Component {
static propTypes = {
containerElement: PropTypes.element
}
static defaultProps = {
containerElement: <div />
};
componentDidMount() {
console.log(this.el);
}
render() {
const containerProps = {
ref: (el) => this.el = el
};
return React.cloneElement(containerElement, containerProps, Component);
};
}
}
I then wrap a component like this:
export default AnimationComponent(reduxForm({
form: 'newResultForm',
validate
})(NewResultForm));
But when I log the element in componentDidMount it is an empty <div/>.
Why is the passed in component not a child of the newly created container element?
Your method of writing a Higher Order Component is a little unorthodox. React developers typically don't have to write functions that accept components and return a new class definition unless they're writing something like redux-form itself. Perhaps instead of passing Component as an argument, see if passing it in props.children will work for you:
<AnimationComponent>{NewResultForm}</AnimationComponent>
I'd define AnimationComponent like the following:
export default class AnimationComponent extends React.Component {
static propTypes = {
containerElement: React.PropTypes.element
};
static defaultProps = {
containerElement: <div />
};
render () {
// For each child of this component,
// assign each a ref and store it on this component as this[`child${index}`]
// e.g. this.child1, this.child2, ...
// Then, wrap each child in the container passed in on props:
return React.Children.map(this.props.children, (child, index) =>
React.cloneElement(
this.props.containerElement,
{ref: ref => this[`child${index}`] = ref},
React.cloneElement(child)
)
);
}
}
Instead of wrapping the form component in AnimationComponent, just export the connected form class:
export default reduxForm({
form: 'newResultForm',
validate
})(NewResultForm));
Now instead of being stuck with how AnimationComponent was configured in NewResultForm's file, we can configure it to our liking where we end up rendering the form. In addition to providing flexibility, the information needed to configure AnimationComponent will be more pertinent where it gets rendered:
export default class MyApp extends React.Component {
render() {
return (
<AnimationComponent containerComponent="span">
<NewResultForm />
</AnimationComponent>
);
}
}
I hope this helped!

Resources