Material UI's withTheme() hides component from ref prop - reactjs

For the rare times when you need a reference to another JSX element in React, you can use the ref prop, like this:
class Widget extends React.PureComponent {
example() {
// do something
}
render() {
...
<Widget ref={r => this.mywidget = r}/>
<OtherWidget onClick={e => this.mywidget.example()}/>
Here, the Widget instance is stored in this.mywidget for later use, and the example() function can be called on it.
In Material UI, you can wrap components around a withTheme() call to make the theme accessible in their props:
export default withTheme()(Widget);
However if this is done, the ref receives an instance of WithTheme rather than Widget. This means the example() function is no longer accessible.
Is there some way to use ref with a component wrapped by withTheme() so that the underlying object can still be accessed, in the same manner as if withTheme() had not been used?
Here is an example demonstrating the issue. Lines 27 and 28 can be commented/uncommented to see that things only fail when the withTheme() call is added.

In order to get the ref of the component which is wrapped with withStyles, you can create a wrapper around Widget, and use that with withStyles like
const WithRefWidget = ({ innerRef, ...rest }) => {
console.log(innerRef);
return <Widget ref={innerRef} {...rest} />;
};
const MyWidget = withTheme()(WithRefWidget);
class Demo extends React.PureComponent {
constructor(props) {
super(props);
this.mywidget = null;
}
render() {
return (
<React.Fragment>
<MyWidget
innerRef={r => {
console.log(r);
this.mywidget = r;
}}
/>
<Button
onClick={e => {
e.preventDefault();
console.log(this.mywidget);
}}
variant="raised"
>
Click
</Button>
</React.Fragment>
);
}
}
Have a look at this answer to see an other alternative approach
losing functions when using recompose component as ref

This is a shorter alternative based on Shubham Khatri's answer. That answer works when you can't alter the inner component, this example is a bit shorter when you can modify the inner component.
Essentially ref doesn't get passed through withTheme() so you have to use a prop with a different name, and implement ref functionality on it yourself:
class Widget extends React.PureComponent {
constructor(props) {
props.ref2(this); // duplicate 'ref' functionality for the 'ref2' prop
...
const MyWidget = withTheme()(Widget);
...
<MyWidget
ref2={r => {
console.log(r);
this.mywidget = r;
}}
/>

Related

Use state from a Context inside Class component

here is my Codepen which illustrates my current problem:
I woud like to use the class component, so I can call the forward function from parentComponents (through ref), but I currently cant figure out how to manipulate the context (Where the current state of the application is stored.
Can somebody help me ?
https://codesandbox.io/s/gallant-dust-vtp46?file=/src/App.tsx:0-1918
Kind regards
I don't know the exactly answer, but I have a solution, that you can forward function to parent component with Functional component. Check this code:
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
const ChildComponent(props, ref) => {
useImperativeHandle(ref, () => {
const funcCallFromParent = (params) => {
funcToHandle(params);
};
return { funcCallFromParent };
});
const doSomething(params) {
...
}
...
}
and then call if from your ParentComponent
...
childRef.current.funcCallFromParent(params);
...
This way will help you use Functional Component instead of Class Component, therefore easier to access the context.
Additional, maybe you'll want to try Redux, it's a good and most popular Context Management for ReactJS.
To consume React Context with class-based component you wrap them with the Consumer component and inject context value as props.
SStateButton
export class SStateButton extends Component {
refe;
name;
onclick;
constructor({
refe,
name,
onclick
}: {
refe: Status;
name: string;
onclick: any;
}) {
super({ refe, name, onclick });
this.refe = refe;
this.name = name;
this.onclick = onclick;
}
forwardToStatus = () => {
if (this.onclick) {
this.onclick(this.refe);
}
};
render() {
return (
<button
className="btn btn-light btn-outline-dark"
onClick={this.forwardToStatus}
>
ClassComponent {this.name}
</button>
);
}
}
App - Given context value value={[status, setStatus]}
<StateContext.Consumer>
{([, setStatus]) => (
<SStateButton
refe="1 Page"
name="Go to Page 1"
onclick={(val) => {
console.log("Additional functions added");
setStatus(val);
}}
/>
)}
</StateContext.Consumer>
Since SStateButton is being used in the same component that is providing the context value, and has the status state and updater function already in scope, the setStatus callback can also just be enclosed in the onclick callback and work just the same.
<SStateButton
refe="1 Page"
name="Go to Page 1"
onclick={(val) => {
console.log("Additional functions added");
setStatus(val);
}}
/>

Function wrapper preventing componentDidUpdate?

I have a component FancySpanComponent:
class FancySpanComponent extends React.Component {
render() {
return (
<span ref={this.props.innerRef} className={this.props.className }>
<div style={{ textAlign: "center" }}>rand: {this.props.randomData}</div>
</span>
);
}
}
which I place into a logger wrapper PropsLogger, and that wrapper is created though React.forwardRef() so my main container may play with what the span inside FancySpanComponent (I’m learning React and test whatever situation I can, so please don’t judge ^^'):
class PropsLogger extends React.Component {
componentDidMount() {
console.log("(Logger) mounting");
}
componentDidUpdate(prevProps) {
console.log("(Logger) Previous:", prevProps);
console.log("(Logger) New:", this.props);
}
render() { return <FancySpanComponent innerRef={this.props.innerRef} {...this.props} />; }
}
const FancySpan = React.forwardRef((props, ref) => <PropsLogger innerRef={ref} {...props} />);
And it works perfectly fine: when a props changes, everything is re-rendered and the old and new props are longed into the console. But if I want to make that wrapper more flexible and work for any component, I have to wrap itself into a function so I can pass the child class name and create it in the logger render() function (that very example comes from the reactjs.org doc):
function propsLoggerWrapper(ClassToLog, props, ref) {
class PropsLogger extends React.Component {
componentDidMount() {...}
componentDidUpdate(prevProps) {...}
render() { return <ClassToLog innerRef={ref} {...props} />; }
}
return <PropsLogger />;
};
const FancySpan = React.forwardRef((props, ref) => propsLoggerWrapper(FancySpanComponent, props, ref));
The problem, in that case, is that each time props change, I don’t know why but my logger wrapper and its child are not technically updated: they are dismounted and remounted, thus logger componentDidUpdate() is never fired. Do you have a clue why this is happening in the case of that function call?
Thank you very much in advance.
Try this approach. The HoC approach you were trying seems incorrect.
You have to return a class from inside the function. So that it could be uses like a React Component.
function propsLoggerWrapper(ClassToLog) {
return class extends React.Component {
componentDidMount() {}
componentDidUpdate(prevProps) {}
render() {
return <ClassToLog {...this.props} ref={this.props.innerRef} />;
}
};
const LoggedSpan = propsLoggerWrapper(Span);
const LoggedSpanWithRef = React.forwardRef((props, ref) => (
<LoggedSpan innerRef={ref} {...props} />
));
Then use it like this, create the spanRef yourself.
<LoggedSpanWithRef ref={this.spanRef} someprop={1} />
Check this Code Sandbox for a workking example
The reason it's unmounting/remounting is that every time FancySpan renders, it's creating a brand new class. A new class means it's a new type of component, and the main way react decides whether to unmount is by whether the types are different. The two types of components consist of identical code, but react doesn't know that.
So what you'll need to do is create the component only once. That means you're going to have to create the component before you know what the values are for the props and ref, so they'll need to be passed in later:
function propsLoggerWrapper(ClassToLog) {
class PropsLogger extends React.Component {
componentDidMount() {...}
componentDidUpdate(prevProps) {...}
render() { return <ClassToLog {...this.props} />; }
}
return PropsLogger;
}
const Temp = propsLoggerWrapper(FancySpanComponent);
const FancySpan = React.forwardRef((props, ref) => <Temp {...props} innerRef={ref} />);
This code seems also to work fine (same as suggested but without the need of an intermédiate temporary object):
function propsLoggerWrapper(ClassToLog) {
class PropsLogger extends React.Component {
componentDidMount() {...}
componentDidUpdate(prevProps) {...}
render() { return <ClassToLog {...this.props} />; }
}
return React.forwardRef((props, ref) => <PropsLogger {...props} innerRef={ref} />);
}
const FancySpan = propsLoggerWrapper(FancySpanComponent);

How do I call a method within a Child component from the Parent in React Native

How do I call a method within a Child component from the Parent in React Native? What I essentially want to do is emulate what componentDidMount() does for class components in a functional component.
I've been getting the error "Function components cannot be given refs" and that I may want to use React.ForwardRef().
ps. idk how i would go about reformatting the child observer, pardon the bad formatting
class Dashboard extends Component {
constructor(props) {
super(props);
this.load = React.createRef();
componentDidMount() {
this.load.current.loadAudio();
}
render(){
latestEP.query = ref => ref.orderBy("id", "desc").limit(1);
return(
{latestEP.docs.map(doc => (
<DashboardItem key={doc.id} doc={doc} ref={this.load} />
))}
)
}
}
const DashboardItem = observer(({ doc }) => {
function loadAudio(){
return console.log("works")}
return (// stuff that requires loadAudio to run first)
})
You can achieve that by using useImperativeHandle hook. Please check this out:
https://reactjs.org/docs/hooks-reference.html#useimperativehandle
Wrap DashItem in forwardRef and implement useImperativeHandle hook like below:
const DashItem = React.forwardRef(({doc}, ref) => {
useImperativeHandle(ref, () => ({
loadAudio: () => {
return console.log("works");
}
}));
...
The error "Function components cannot be given refs" should be self-explanatory: you need to change DashItem to be a class component instead of a functional component.

Combination of Mobx-state-tree, Mobx-react and RenderProps not updating react component

This is a complex problem. Hope I manage to explain so everyone can understand the bug
1) Parent component.
#observer
export class Edit extends React.Component<IProps> {
article: any
componentWillMount() {
const articleId = this.props.routerStore!.routerState.params.id!
this.article = ArticleModel.create({ id: articleId || null })
this.article.init()
}
render() {
if (!this.article) {
return <div>loading...</div>
}
return <div className='main-wrapper'>
<EditArticle article={this.article} />
</div>
}
}
2) Child Component
It uses the react-beautiful-dnd to allow drag and drop on the article layout. This library uses RenderProps (provided: any, snapshot)
#observer
export class EditArticle extends React.Component<IProps> {
render() {
return <div className='main-column article-target'>
<Droppable
ignoreContainerClipping
direction={'vertical'}
droppableId='article'
key={'article'}
>
{(provided: any, snapshot: any) => {
return <ObservedDragDropContext
provided={provided}
snapshot={snapshot}
>
{this.props.article.layout.map((el: any, index: number) => {
return <WithDragable
key={getPath(el)}
id={getPath(el)}
index={index}
remove={() => {}}
>
<div className='layout-item'>
<Layout key={getPath(el)} item={el} />
</div>
</WithDragable>
})}
</ObservedDragDropContext>
}}
</Droppable>
</div>
}
}
Article is a mobx-state-tree model.
This is the init method of the actions property of the model:
init() {
const run = flow(function*() {
const res = yield getArticle(self.id)
applySnapshot(self, res)
})
run()
},
The flow is the following:
1) create the article
2) init the article
3) I expect to see the article updated after I get the response and applySnapshot on the article
But the child component doesn't get rerendered after applySnapshot is run. I checked and applySnapshot runs and the article has the updated values received from backend.
I assume the problem is because I use RenderProps in my child component and this interferes in some way and restricts the component from rerendering.
The tricky bit is that if I add
{this.props.article.layout.map((el: any, index: number) =>
<Layout key={getPath(el)} item={el} />
)}
in the child component in the render function anywhere, this will fix the problem and the initial component gets updated. So just by rerendering the props, the child component rerenders itself and implicitly fixes my initial bug by rerendering the Drappable and the RenderProps.
Possible Solution 1: if I use a different react DND library that is not using RenderProps everything updates as it should. But I don't want to do that because react-beautiful-dnd has nice animations and I would need to refactor some of the code.
I haven't been able to understand why the Child component is not rerendered.
Is the Child Component not aware that it depends on the article because I use the this.props.article only inside the renderProps?
Can I avoid using the renderProps?

Joining React props with custom props

When defining props in React, using Typescript, it seems that the default React props get overwritten by whatever the interface is. Is there a clean way to merge the two without having to specify every prop React already knows about?
Example:
interface IProps { // extends React.???
title: string;
// Generally seen adding children?: any
// But this can get out of hand with onClick, onChange, etc
}
function MyComponent(props: IProps) {
return props.children; // Typescript error: children doesn't exist on props
}
What you're referring to as "React default props" aka "every prop React already knows about" are more properly called "props accepted by any React DOM element wrapper component", i.e. onClick, className, etc.
Default props typically refers to the static defaultProps property on a React.Component, with which you provide default values for any props that were not passed to your component.
onClick, className, etc. are not reserved prop names and you can use them however you want in your own components, for instance you could have your component expect className to be a function (regardless of whether it's a good idea). The only reserved prop names that work on React elements of any kind (at the time of writing) are key and ref, and they're not really true props because they're not available to your component's render method.
Passing onClick to your own component does not automatically register a click handler. It will only do so if you pass the onClick you received to a <div>, <button>, or other React DOM Element wrapper that you render somewhere down the line. If you don't do anything with a prop you were passed, it has no effect (besides possibly causing a pure render component to update when it otherwise wouldn't).
For example, the following component will not respond to clicks:
const ClickFail = props => <button />
render(<ClickFail onClick={...} />, document.getElementById('root'))
But the following will:
const ClickSuccess = props => <button onClick={props.onClick} />
render(<ClickSuccess onClick={...} />, document.getElementById('root'))
And you could pass onClick to only one subelement if you really wanted:
const ClickButtonOnly = props => (
<form>
<input placeholder="ignores clicks" />
<button onClick={props.onClick}>Handles Clicks</button>
</form>
)
Or you could pass in multiple click handlers with different names:
const SimpleForm = props => (
<form>
<button onClick={props.onCancelClick}>Cancel</button>
<button onClick={props.onOKClick}>OK</button>
</form>
)
Also keep in mind that some DOM element wrappers accept props that others do not, for instance readOnly applies only to <input> and <textarea>.
You can even require children to be whatever type you want. For instance, you can pass a function as the children of a component and use it (again, not the best use of React, but just to illustrate what's possible):
type Props = {
value: number,
children: (value: number) => number,
}
const ApplyFunction = (props: Props) => (
<div>{React.Children.only(props.children)(props.value)}</div>
)
render(
<ApplyFunction value={3}>
{value => value * value}
</ApplyFunction>,
document.getElementById('root')
)
// renders <div>9</div>
So you see, IProps does not necessarily have to extend anything.
However, it is common to pass along rest props to a React DOM Element wrapper (e.g. <div {...props}>...</div> and as you were asking, it would be nice to be able to check the type of all of those input properties to your component.
I think you could do the following with Flow to check the types correctly, but unfortunately I don't think there's any Typescript equivalent (someone correct me if I'm wrong):
type Props = React.ElementProps<typeof 'div'> & {
title: string,
}
const MyComponent = (props: Props) => (
<div {...props}>
{props.title}
</div>
)
You should define that your stateless functional component will return React.SFC<YourProps>.
Try this
import * as React from "react";
const MyComponent: React.SFC<IProps> = (props) => {
return props.children;
}
If you want to use class-based component, you can extend your class with React.Component<YourProps(optional), YourState(optional)> instead
For example
import * as React from "react"
class MyComponent extends React.Component<IProps> {
public render(): JSX.Element {
return (
<div>...</div>
);
}
}
type TitleProps = { // your custom props
level?: level;
color?: string;
};
const Title = (props: TitleProps & React.Component['props']) => { // join default props and custom props
const { level = 'h1', color, children } = props; // joined props containing default props
return <Text style={[styles[level], color && { color }]}>{children}</Text>;
}
I was able to solve the problem by this way.

Resources