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!
Related
In my React/Typescript project, I have a challenge:
From the Child component I pass a prop class down:
Child class={{ myBanner: styles.myBanner } />
I typed class prop as follows:
import { SerializedStyles } from '#emotion/react';
import { Class as MyCustomBannerClass } from './MyBanner/MyBanner.types';
type Class = Partial<Record<'root', SerializedStyles>> & {
myBanner: MyCustomBannerClass;
};
export type Props = {
class: Class;
};
Inside Child component I have <MyBanner/> component, where I also have a class prop:
export type Class = Partial<Record<'root', SerializedStyles>>;
export type Props = {
class?: Class;
};
<MyBanner class={props.class?.myBanner} />
This is all working fine.
Now from within Parent component, through Child and <MyBanner/>, I am able to override a css style in Parent from <MyBanner /> component.
The challenge:
Now I have a case:
Inside Child, I have another child <AnotherChild/>.
And within <AnotherChild />, I have <MyBanner/> component.
Question:
How do I pass and type class={{ myBanner: styles.myBanner } through both...
Child:<Child class={{ myBanner: styles.myBanner } />
And AnotherChild: <AnotherChild class={???} />
...and pass it down to <MyBanner class={props.class?.myBanner} />?
This looks like a problem that can easily be solved by using React.useContext ...in other words, the ContextAPI. Here's the official docs about it.
To tackle your issue specifically, you can useContext like this:
Parent.tsx
/** PARENT **/
import React from 'react';
import { SerializedStyles } from '#emotion/react';
import { Class as MyCustomBannerClass } from './MyBanner/MyBanner.types';
// define parent context
type Class = Partial<Record<'root', SerializedStyles>> & {
myBanner: MyCustomBannerClass;
};
interface Props {
parentClass?: Class,
};
export const ParentContext = React.createContext({} as Props);
export const Parent = props => {
const parentClass = React.useRef({ myBanner: styles.myBanner });
const contextProps = {
parentClass: ..., // add props to be passed down
};
return (
// We pass (or literally provide) "parentClass" (and more)
// to all the children of "Parent"
<ParentContext.Provider value={{ ...contextProps }}>
<Child />
</ParentContext.Provider>
)
};
Child.tsx
// FIRST-CHILD
import React from 'react';
// define context
type Class = Partial<Record<'root', SerializedStyles>>;
interface Props {
childClass?: Class,
};
export const ChildContext = React.createContext({} as Props);
// define component
export const Child = props => {
const childClass = React.useRef({ ... })
const contextProps = {
childClass: ..., // add props to be passed down
};
return (
// Both "MyBanner" and "AnotherChild" are provided with both `class` props
// from "Parent" and "Child" and it can go on and on...
<ChildOneContext.Provider value={{ ...contextProps }}>
<MyBanner />
<AnotherChild />
</ChildOneContext.Provider>
)
};
AnotherChild.tsx (You can do the same in "MyBanner")
// SECOND-CHILD
import React from 'react';
import { ParentContext } from './Parent';
import { ChildContext } from './Child';
// define component
const AnotherChild = props => {
const { parentClass } = React.useContext(ParentContext);
const { childClass } = React.useContext(ChildContext);
return (
<div className={ParentClass}>
<div className={childClass}></div>
</div>
)
};
Note:
If you want a child component to override the class prop in any case, you can define the classes with useState instead of useRef as I've done in this case.
That said, at the back of your mind, remember that ContextAPI is designed to share data that can be considered “global” for a tree of React components, such as your case!
I used React.createRef() to call child method, like that
import Child from 'child';
class Parent extends Component {
constructor(props) {
super(props);
this.child = React.createRef();
}
onClick = () => {
this.child.current.getAlert();
};
render() {
return (
<div>
<Child ref={this.child} />
<button onClick={this.onClick}>Click</button>
</div>
);
}
}
Child class like that
export default class Child extends Component {
getAlert() {
alert('getAlert from Child');
}
render() {
return <h1>Hello</h1>;
}
}
It works well. But when I want to use i18next to translate child component, I have to add withTranslation() to use HOC.
import { useTranslation } from 'react-i18next';
class Child extends Component {
getAlert() {
alert('getAlert from Child');
}
render() {
const { t } = this.props;
return <h1>{t('Hello')}</h1>;
}
}
export default withTranslation()(Child);
Then return error: Function components cannot be given refs.
Means cannot use ref in <Child /> tag. Is there any way to call child function after add i18next?
This is a problem since the withTranslation HOC is using a function component. By wrapping your Child component with a HOC you essentially are placing the ref on the withTranslation component (by default).
There are multiple ways to fix this problem, here are the two easiest:
Using withRef: true >= v10.6.0
React-i18n has a built in option to forward the ref to your own component. You can enable this by using the withRef: true option in the HOC definition:
export default withTranslation({ withRef: true })(Child);
Proxy the ref using a named prop
Instead of using <Child ref={this.child} />, choose a different prop to "forward" the ref to the correct component. One problem though, you want the ref to hold the component instance, so you will need to assign the ref manually in the lifecycle methods.
import Child from 'child';
class Parent extends Component {
constructor(props) {
super(props);
this.child = React.createRef();
}
onClick = () => {
this.child.current.getAlert();
};
render() {
return (
<div>
<Child innerRef={this.child} />
<button onClick={this.onClick}>Click</button>
</div>
);
}
}
import { useTranslation } from 'react-i18next';
class Child extends Component {
componentDidMount() {
this.props.innerRef.current = this;
}
componentWillUnmount() {
this.props.innerRef.current = null;
}
getAlert() {
alert('getAlert from Child');
}
render() {
const { t } = this.props;
return <h1>{t('Hello')}</h1>;
}
}
export default withTranslation()(Child);
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);
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
I am trying to use my url as a parameter by passing the Match object into my react component class. However it is not working! What am I doing wrong here?
When I create my component as a JavaScript function it all works fine, but when I try to create my component as a JavaScript class it doesn't work.
Perhaps I am doing something wrong? How do I pass the Match object in to my class component and then use that to set my component's state?
My code:
import React, { Component } from 'react';
import axios from 'axios';
import PropTypes from 'prop-types';
class InstructorProfile extends Component {
constructor(props, {match}) {
super(props, {match});
this.state = {
instructors: [],
instructorID : match.params.instructorID
};
}
componentDidMount(){
axios.get(`/instructors`)
.then(response => {
this.setState({
instructors: response.data
});
})
.catch(error => {
console.log('Error fetching and parsing data', error);
});
}
render(){
return (
<div className="instructor-grid">
<div className="instructor-wrapper">
hi
</div>
</div>
);
}
}
export default InstructorProfile;
React-Router's Route component passes the match object to the component it wraps by default, via props. Try replacing your constructor method with the following:
constructor(props) {
super(props);
this.state = {
instructors: [],
instructorID : props.match.params.instructorID
};
}
Hope this helps.
Your constructor only receives the props object, you have to put match in it...
constructor(props) {
super(props);
let match = props.match;//← here
this.state = {
instructors: [],
instructorID : match.params.instructorID
};
}
you then have to pass that match object via props int a parent component :
// in parent component...
render(){
let match = ...;//however you get your match object upper in the hierarchy
return <InstructorProfile match={match} /*and any other thing you need to pass it*/ />;
}
for me this was not wrapping the component:
export default (withRouter(InstructorProfile))
you need to import withRouter:
import { withRouter } from 'react-router';
and then you can access match params via props:
someFunc = () => {
const { match, someOtherFunc } = this.props;
const { params } = match;
someOtherFunc(params.paramName1, params.paramName2);
};
Using match inside a component class
As stated in the react router documentation. Use this.props.match in a component class. Use ({match}) in a regular function.
Use Case:
import React, {Component} from 'react';
import {Link, Route} from 'react-router-dom';
import DogsComponent from "./DogsComponent";
export default class Pets extends Component{
render(){
return (
<div>
<Link to={this.props.match.url+"/dogs"}>Dogs</Link>
<Route path={this.props.match.path+"/dogs"} component={DogsComponent} />
</div>
)
}
}
or using render
<Route path={this.props.match.path+"/dogs"} render={()=>{
<p>You just clicked dog</p>
}} />
It just worked for me after days of research. Hope this helps.
In a functional component match gets passed in as part of props like so:
export default function MyFunc(props) {
//some code for your component here...
}
In a class component it's already passed in; you just need to refer to it like this:
`export default class YourClass extends Component {
render() {
const {match} = this.props;
console.log(match);
///other component code
}
}`