Suppose you have a simple react application (see my codesandbox):
import React from 'react';
import { render } from 'react-dom';
class RenderPropComponent extends React.Component {
render() {
console.log(this.example.test());
return this.props.render();
}
}
class Example extends React.Component {
test = () => console.log('Test successful!');
render() {
return <h1>I am an example!</h1>;
}
}
const App = () => (
<RenderPropComponent
render={() => {
return (
<Example ref={node => this.example = node} />
)
}}
/>
);
render(<App />, document.getElementById('root'));
This causes the error:
TypeError
Cannot read property 'test' of undefined
How can I assign a ref to a component rendered via render prop?
I know I can accomplish this with this.props.children as follows:
class RenderPropComponent extends React.Component {
const childComponents = React.Children.map(this.props.children, child => {
return React.cloneElement(child, {ref: node => this.example = node})
});
console.log(this.example.test());
render() {
return <div>{childComponents}</div>;
}
}
...
<RenderPropComponent>
<Example />
</RenderPropComponent>
But I would like to be able to use a render prop to do this! Any suggestions?
Not fully sure if it fits your case, but maybe you can pass the setRef function as an argument to the render prop? Like in this forked sandbox.
import React from 'react';
import { render } from 'react-dom';
const styles = {
fontFamily: 'sans-serif',
textAlign: 'center',
};
class Hello extends React.Component {
constructor(props) {
super(props);
this.setRef = this.setRef.bind(this);
}
setRef(node) {
this.example = node;
}
render() {
console.log(this.example && this.example.test());
return this.props.render(this.setRef);
}
}
class Example extends React.Component {
test = () => console.log('Test successful!');
render() {
return <h1>I am an example!</h1>;
}
}
const App = () => (
<div style={styles}>
<Hello
name="CodeSandbox"
render={(setRef) => {
return (
<Example ref={setRef} />
)
}}
/>
<h2>Start editing to see some magic happen {'\u2728'}</h2>
</div>
);
render(<App />, document.getElementById('root'));
The only problem i see here, is that on initial render this.example will not be available (that's why I've added a guard to the console log) and after it will be set, the rerender will not be triggered (since it's set on the instance and not in the state). If a rerender is needed, we can store the ref in the component state or force the rerender.
On the other hand, if you need a ref to be used in some event handler later on, that should do the trick without rerendering.
Look carefully, this keyword is used in the global scope, not in the scope of the example component.
const App = () => (
<RenderPropComponent
render={() => {
return (
<Example ref={node => this.example = node} />
)
}}
/>
);
If you didn’t spot it yet, take a look at that snippet:
class Foo {
constructor(stuffToDo) {
this.bar = ‘bar’;
this.stuffToDo = stuffToDo
}
doStuff() {
this.stuffToDo();
}
}
new Foo(() => console.log(this.bar)).doStuff();
This will log undefined, not bar, because this is derived from the current closure.
Related
I'm successfully passing a React ref through a factory function and down the component hierarchy to a child component that renders a <canvas>. For the factory function, I'm using forwardRef to pass the ref to the created component.
The following works as I can see the simple green rectangle in the browser, rendered by the child component ObservationPlotCanvas
App.jsx
import { createRef, Component } from 'react'
import ObservationPlotFactory from './ObservationPlotFactory'
class App extends Component {
constructor(props) {
super(props)
this.canvasRef = createRef()
}
render() {
const factory = ObservationPlotFactory({ config: { something: true } })
return (
<div className="App">
<header className="App-header"></header>
<factory.Component ref={this.canvasRef} />
</div>
)
}
}
export default App
ObservationPlotFactory.jsx
import React from 'react'
import ObservationPlot from './ObservationPlot'
const ObservationPlotFactory = config => {
return {
Component: forwardRef((props, ref) => {
// do something with config ...
return <ObservationPlot ref={ref} {...props} />
}),
Key: props => {
return <div>Key</div>
},
}
}
export default ObservationPlotFactory
ObservationPlot.jsx
import React, { forwardRef, Component } from 'react'
import ObservationPlotCanvas from './ObservationPlotCanvas'
export class ObservationPlot extends Component {
render() {
return <ObservationPlotCanvas ref={this.props.innerRef} />
}
}
export default forwardRef((props, ref) => <ObservationPlot innerRef={ref} {...props} />)
ObservationPlotCanvas.jsx
import React, { forwardRef, Component } from 'react'
class ObservationPlotCanvas extends Component {
componentDidMount() {
this.draw()
}
componentDidUpdate() {
this.draw()
}
render() {
return (
<div
style={{
position: 'absolute',
top: `100px`,
left: `100px`,
}}
>
<canvas width="600" height="400" ref={this.props.innerRef} />
</div>
)
}
draw() {
if (!this.props?.innerRef?.current) {
return
}
const g = this.props.innerRef.current.getContext('2d')
g.fillStyle = 'green'
g.fillRect(10, 10, 150, 100)
}
}
export default forwardRef((props, ref) => <ObservationPlotCanvas innerRef={ref} {...props} />)
But in the root level App, these components are rendered as a list using map and need to be created dynamically. I'm aware of Callback Refs and normally I would do something like this in the App constructor:
this.canvasRef = []
this.setCanvasRef = index => element => {
this.canvasRef[index] = element;
}
and then:
ref={(el) => this.setCanvasRef(index)}
on the element of interest in the render. But doing this provides the callback function when the child looks at this.props.innerRef and it needs the actual element to reference for it's canvas drawing.
I needed to add a language change to the website, I used context api, but when I used Provider to pass the state it did not return anything
LocalSwitch location: src/components/LocalSwitch/LocalSwitch
Home location: src/pages/Home/Home.js
LocaleSwitch.js
export const Context = React.createContext(require('../../locales/eng.json'))
class LocaleSwitch extends Component {
state = {
locale:{}
}
_update = lang =>{
this.setState({
locale:require(`../../locales/${lang}.json`),
}, ()=>{console.log(`${lang}`) })
}
render = ()=> {
const {Provider} = Context;
return (
<Provider value={this.state}>
<Wrapper>
{icons.map(icon=>(
<img
key={icon}
src= {require(`../../assets/${icon}.png`)}
alt= {icon}
className="locale"
onClick={()=> this._update(icon)}
/>
))}
</Wrapper>
</Provider>
);
}
}
Home.js
import {Context} from "../../components/LocaleSwitch/LocaleSwitch";
class Home extends Component {
state = {
locale:{}
}
render = ()=> {
const {Consumer} = Context;
return (
<Consumer>
{({locale})=>(
<Wrapper>
<Header>
<Hero id="hero"/>
<HeaderTitle id="title">
{locale.titleP1} <br/> {locale.titleP2}
<p>{locale.subtitle}<br/>{locale.subtitle2}</p>
</HeaderTitle>
</Wrapper>
)}
</Consumer>
);
}
}
It doesn't look like you have a Provider somewhere as a parent for your Consumer in your Home.js. You probably want to wrap the context provider somewhere at the root of your app.
In this case the Home component would have to rendered inside of the LocaleSwitch component.
Try to add the Provider functionality in Home.js and you'll see what I mean.
const Locale = React.createContext('english')
class Home extends React.Component {
render() {
return (
<Locale.Provider>
<Locale.Consumer>
{
// Should print 'english'
(locale) => {locale}
}
</Locale.Consumer>
</Locale.Provider>
)
}
}
what you are effectively doing is
const Locale = React.createContext('english')
class LocaleSwitch extends React.Component {
render() {
return (
<Locale.Provider>
<div>I could be a locale consumer if i wanted to </div>
</Locale.Provider>
)
}
}
class Home extends React.Component {
render() {
// I have no provider
return (
<Locale.Consumer>
{
(locale) => {locale}
}
</Locale.Consumer>
)
}
}
So I am using React's context because I have to change a state in the opposite direction.
E.g.:
App.js (has state) <--- My Component (changes the state in App.js)
I know how to do this using an onClick event. However, I fail understanding how to do this in a componentDidMount(). I created a basic example to illustrate what I'm trying to achieve:
MyComponent.js
import { MyConsumer } from '../App.js';
export default class MyComponent extends Component {
componentDidMount() {
// TRYING TO CHANGE STATE IN COMPONENTDIDMOUNT
<MyConsumer>
{({ actions }) => actions.setMyState(true)}
</MyConsumer>
}
render() {
return (
<SearchConsumer>
{({ actions }) => {
return (
<div onClick={() => actions.setMyState(true)}>
My content
</div>
)
}}
</SearchConsumer>
)
}
}
App.js
export const SearchContext = createContext();
export const SearchProvider = SearchContext.Provider;
export const SearchConsumer = SearchContext.Consumer;
class App extends Component {
constructor (props) {
super (props)
this.state = {
setMyState: 0,
}
}
render(){
return(
<SearchProvider value={
{
actions: {
setMyState: event => {
this.setState({ setMyState: 0 })
},
}
}
}>
<Switch>
<Route
exact path='/' render={(props) => <MyComponent />}
/>
</Switch>
</SearchProvider>
)
}
}
If you're using react 16.6.0 or later and are using exactly one context consumer, then the simplest approach is to use contextType (note that that's singular, not plural). This will cause react to make the value available on this.context, which you can then use in lifecycle hooks. For example:
// In some other file:
export default MyContext = React.createContext();
// and in your component file
export default class MyComponent extends Component {
static contextType = MyContext;
componentDidMount() {
const { actions } = this.context;
actions.setMyState(true);
}
// ... etc
}
If you are on an older version and thus can't use contextType, or if you need to get values from multiple contexts, you'll instead need to wrap your component in another component, and pass the context in via a prop.
// In some other file:
export default MyContext = React.createContext();
// and in your component file
class MyComponent extends Component {
static contextType = MyContext;
componentDidMount() {
const { actions } = this.props;
actions.setMyState(true);
}
// ... etc
}
export default props => (
<MyContext.Consumer>
{({ actions }) => (
<MyComponent actions={actions} {...props} />
)}
</MyContext.Consumer>
);
I fixed my problem by an idea given thanks to Nicholas Tower's answer. Instead of using the contextType in React, I just passed my actions as a prop in a different component. This way I could still use everything of my consumer if I just pass it on as a prop.
class MyComponent extends Component {
componentDidMount() {
this.props.actions.setMyState(true);
}
// ... etc
}
export default class MyComponentTwo extends Component {
render(){
return(
<MyConsumer>
{({ actions }) => (
<MyComponent actions={actions}/>
)}
</MyConsumer>
)
}
);
This is my code
import React, { Component } from "react";
import { render } from "react-dom";
class CView extends Component {
someFunc() {
alert(1);
}
render() {
return <div>Hello, there</div>;
}
}
class App extends Component {
getControl() {
this.cv = <CView />;
return this.cv;
}
render() {
return (
<div>
<h2 onClick={() => this.cv.someFunc()}>Click Me</h2>
{this.getControl()}
</div>
);
}
}
render(<App />, document.getElementById("root"));
Also available on https://codesandbox.io/s/k2174z4jno
When I click on the h2 tag, I get an error saying someFunc is not defined. How can I expose a function of a component so that other components can access it?
Thanks
I think that this.cv = <CView />; will not directly return instance of CView component.
onClick={() => {
console.log(this.cv instanceof CView); // false
this.cv.someFunc();
}}
But if you try to use refs you will access it.
class App extends Component {
constructor(props) {
super(props)
this.cv = React.createRef();
}
onClick() {
this.cv.current.someFunc();
}
render() {
return (
<div>
<h2 onClick={() => this.onClick()}>Click Me</h2>
<CView ref={this.cv} />
</div>
);
}
}
It is more "React way" though. https://codesandbox.io/s/vy61q9o8xy
I´m trying to build a ReactJS high order component using ES6 syntax. Here is my try:
export const withContext = Component =>
class AppContextComponent extends React.Component {
render() {
return (
<AppContextLoader>
<AppContext.Consumer>
{context => <Component {...props} context={context} />}
</AppContext.Consumer>
</AppContextLoader>
);
}
};
Here, AppContextLoader gets context from database and provide it to the context, as:
class AppContextLoader extends Component {
state = {
user: null,
};
componentWillMount = () => {
let user = databaseProvider.getUserInfo();
this.setState({
user: user
});
};
render = () => {
return (
<AppContext.Provider value={this.state}>
{this.props.children}
</AppContext.Provider>
);
};
}
export default AppContextLoader;
And usage:
class App extends Component {
static propTypes = {
title: PropTypes.string,
module: PropTypes.string
}
render = () => {
return (
withContext(
<Dashboard
module={this.props.module}
title={this.props.title}
/>
);
export default App;
For some reason my wrapped component (Dashboard) is not getting my context property, just the original ones (title and module).
How to properly write HOC using ES6 syntax?
You are not using the HOC correctly, you need to pass the component and not the component instance. Also invoking the HOC from within render is a bad patten since each render a new component will be returned, you must write
const DashboardWithContext = withContext(Dashboard);
class App extends Component {
render = () => {
return (
<DashboardWithContext
module={"ADMIN"}
title={"MY APP"}
/>
)
}
}
export default App;
Also in withContext HOC since the returned component is a class, you would access props like {...this.props} instead of {...props}, However it makes sense to use a functional component since you aren't actually using the lifecycle methods
export const withContext = Component => (props) => (
<AppContext.Consumer>
{context => <Component {...props} context={context} />}
</AppContext.Consumer>
);
Working Codesandbox
It should be this.props instead:
<Component {...this.props}
This should be working for you:
render() {
const props = this.props;
return (
<AppContext.Consumer>
{context => <Component {...props} context={context} />}
</AppContext.Consumer>
);
}
You have a few problems:
You're not using the Context API properly - the context is created for the use of a Provider to share a value to one or many Consumers - you are creating with the hoc a new Provider and Consumer.
from your example, you don't need to use Context - use an hoc for new use data - withUserData
You should use this.props and not props
In the usage section, you pass to the hoc an element and not a component
You're not getting the props from withContext
Solution
export const withUserData = BaseComponent =>
class AppContextLoader extends Component {
state = {
user: null,
};
componentWillMount = () => {
let user = databaseProvider.getUserInfo();
this.setState({
user: user
});
};
render = () => {
return (
<BaseComponent {...this.props} {...this.state} />
);
};
}
And usage:
class App extends Component {
static propTypes = {
title: PropTypes.string,
module: PropTypes.string
}
render = () => {
return (
<EnhancedDashboard
module={this.props.module}
title={this.props.title}
/>
);
}
const EnhancedDashboard = withUserData(Dashboard)
export default App;