I want to clone/extend a React component (without knowing if it is statefull or stateless) and pass it props:
const Foo = (props) => {
return (
<div>foo</div>
);
}
class Bar extends React.Component {
render() {
return (
<div>bar</div>
)
}
}
The problem is, these two variables Foo and Bar should be handled differently:
const FooExtended = (props, context) => {
return Foo(_.extend(props, additionalProps), context);
}
class BarExtended extends Bar {
constructor(props, context) {
super(_.extend(props, additionalProps), context);
}
}
And there is no simple way to know if a variable Component is Stateless or Statefull without doing hacky toString regex tests.
React.cloneElement/createElement fails on these giving me the following error:
React.createElement: type is invalid -- expected a string (for
built-in components) or a class/function (for composite components)
but got: object. You likely forgot to export your component from the
file it's defined in.
So is there a simple way that I can do just cloneComponent(originalComponent, additionalProps)?
And there is no simple way to know if a variable Component is Stateless or Statefull [...]
And I think this is one of the reasons why it was required to extend React.Component at some point, to make it easier to distinguish between those two. Because React itself has to be able to distinguish between them since classes cannot be instantiated without new.
You could do the following:
function cloneComponent(originalComponent, additionalProps) {
if (originalComponent.prototype instanceof React.Component) {
return class extends originalComponent {
constructor(props, context) {
super(_.extend(props, additionalProps), context);
}
};
}
return (props, context) => {
return originalComponent(_.extend(props, additionalProps), context);
};
}
Because Foo.protoype instanceof React.Component is true.
However, I think it is more common to do something like this instead:
function addProps(Component, additionalProps) {
return props => <Component {...props} {...additionalProps} />;
}
Then there is no need to distinguish between stateful and stateless components.
Related
Background
I created a fully functional component. The component has state and props, and there are many methods inside. My component should work differently according to the os(ios / android). So I solved this problem by if statement like below.
if( platform.os == 'ios') { ... } else { ... }
The problem was that as the code volume increased, there was a problem with readability, and I decided to make a separate component for IOS and for Android. The first thing that came to mind was inheritance because ES6 and Typescript Support Class. The picture of concept is this.
However, React does not recommend inheritance. So I was just going to hand over the functions overridden by props to the Speech component in the SpeechIOS component's render function.
The code is as follows.
Speech.tsx
type Props = {
team: number,
onSpeechResults: (result: string) => void
...
}
type States = {
active: boolean;
error: string;
result: string;
...
};
export default class Speech extends Component<Props,States> {
state = { ... };
constructor(props: Props) {
super(props);
...
}
// render
render() {
...
return (
<ImageBackground source={require("../images/default-background.jpeg")} style={styles.full}>
...
</ImageBackground>
);
}
sendCommand = (code: number, speed: number, callback?: () => void) => { ... }
getMatchedSpell = (spellWord: string): { code: number, speed: number } => { ... }
onSpeechResults(e: Voice.Results) { ... };
...
}
SpeechIOS.tsx
import Speech from './Speech';
type Props = {}
type States = {}
export default class SpeechIOS extends Component<Props,States> {
constructor(props: Props) {
super(props);
...
}
// render
render() {
...
return ( <Speech team="1" onSpeechResults={this.onSpeechResults}></Speech> );
}
sayHello() {
console.log("Hello!!");
}
// I want that Speech Component call this onSpeechResults function
onSpeechResults(result: string) {
this.setState({...});
let temp = this.getMatchedSpell( ... ); // which is in Speech Component
this.sendCommand( 10, 100 ... ); // which is in Speech Component
this.sayHello(); // which is in SpeechIOS component only
... other things..
};
}
Problem.
As you can see, the onSpeechResults which is in SpeechIOS Component use some functions in Speech Component and in SpeechIOS Component also.
So, How to solve this problem? Should I use Inheritance?
Alternatively, break out any common logic into a utility function like SpeechUtil.ts, a whole new file that holds this shared logic and each util function, and exports them. Then, each component separately imports them. That ensures that if you ever update that logic it affects both components
You should define a top level component that defines the shared props and methods, and use those to render either your ios or android component. pass the props and methods to the children. This is composition which is favored over inheritance in react. example:
class Speech extends React.Component {
state ={shared: 'state'}
sharedMethod = () => {
this.setState({blah: 'blah})
}
render() {
const Root = Platform.select({
ios: SpeechIOS,
android: SpeechAndroid
})
return <Root onClick={this.sharedMethod} shared={this.state.shared}/>
}
}
You can use React.createRef for this purpose.
Below is an example code.
import Speech from './Speech';
type Props = {}
type States = {}
export default class SpeechIOS extends Component<Props, States> {
constructor(props: Props) {
super(props);
this.speech = React.createRef();
}
// render
render() {
...
return (<Speech ref={this.speech} team="1" onSpeechResults={this.onSpeechResults}></Speech>);
}
sayHello() {
console.log("Hello!!");
}
// I want that Speech Component call this onSpeechResults function
onSpeechResults(result: string) {
this.setState({ ...});
let temp = this.speech.current.getMatchedSpell(... ); // which is in Speech Component
this.sendCommand(10, 100 ... ); // which is in Speech Component
this.sayHello(); // which is in SpeechIOS component only
...other things..
};
}
Is the following an anti pattern in React? I like the pattern because it gives me context in static functions when a component has been instantiated. Then later I can import the class and call a static method to modify state. Or can this be done in a better way?
// componentA.js
function bleedContext() {
ComponentA.staticMethod = ComponentA.staticMethod.bind(this)
}
export default class ComponentA {
static staticMethod() {
this.setState({foo: 'bar'})
}
constructor() {
this.state = {}
bleedContext.call(this)
}
render() {
return (
...
)
}
}
// componentB.js
import ComponentA from 'path/to/componentA'
export default class ComponentB {
handleClick() {
ComponentA.staticMethod()
}
render() {
return (
<button onClick={this.handleClick} />
)
}
}
This is clearly an antipattern and possibly a mistake, depending on conditions. Static class method shouldn't operate with class instance. staticMethod is bound to specific component instance and uses setState, this could be only justified a class is a singleton (though a singleton is often an antipattern, too). This will result in bugs and memory leaks if more than one class instance is expected, and every React component is expected to have more than one instance, at least for testing.
A proper way for two independent components to interact with each other in React is to have a common parent component that provides this interaction, e.g.:
class ModalContainer extends Component {
modalRef = React.createRef();
render() {
return <>
<Modal ref={this.modalRef} />
<SomeComponentThatUsesModal modalRef={this.modalRef} />
</>;
}
}
The problem with example above is that this will require to pass modalRef prop deeply if <SomeComponentThatUsesModal> is nested.
This problem is solved with React context or other third-party global state solutions like Redux.
This can be done with React 16.3 context API, considering that Modal class instance has open method:
const ModalContext = React.createContext();
function getModal(modalRef) {
return {
open: data => modalRef.current.open(data);
close: () => modalRef.current.close();
}
}
class ModalContainer extends Component {
modalRef = React.createRef();
render() {
return <>
<Modal ref={this.modalRef} />
<ModalContext.Provider value={getModal(this.modalRef)}>
{this.props.children}
</ModalContext.Provider>
</>;
}
}
Then for any deeply nested component modal object with open and close methods will be available via context:
const SomeComponentThatUsesModal = props => <div>
<ModalContext.Consumer>
{modal => <button onClick={() => modal.open('foo')} />}
</ModalContext.Consumer>
</div>;
<ModalContainer>
...deeply nested component
<SomeComponentThatUsesModal />
...
</ModalContainer>
Here's a demo.
I am trying to create a component that shouldn't when a certain property is true, but should perform a shallow compare (the default for PureComponent).
I've tried doing the following behavior:
export default class ContentsListView extends PureComponent<Props> {
shouldComponentUpdate(props: Props) {
if (props.selecting) {
return false;
}
return super.shouldComponentUpdate(props);
}
render() {
}
}
However, super.shouldComponentUpdate is not defined. Is there some way to "tap into" the shallow compare of PureComponent without writing my own?
You should not implement shouldComponentUpdate when you are extending your component using PureComponent. If however you want to use the shouldComponentUpdate functionality you can simply write a wrapper component around your original component or use HOC to implement your custom shouldComponentUpdate and render the PureComponent
Sample code
class ContentsListView extends PureComponent {
render() {
console.log("render list");
return (
...
);
}
}
export default class Wrapper extends React.Component {
shouldComponentUpdate(props) {
if (props.selecting) {
return false;
}
return true;
}
render() {
return <ContentsListView {...this.props} />;
}
}
You can see a working demo on codesandbox here
There is no super.shouldComponentUpdate in PureComponent because it implements shallow checks by checking isPureReactComponent property, not with shouldComponentUpdate. A warning is issued when both isPureReactComponent and shouldComponentUpdate are in use because shouldComponentUpdate efficiently overrides the behaviour of isPureReactComponent.
React doesn't expose its shallowEqual implementation, third-party implementation should be used.
In case this becomes a common task, own PureComponent implementation can be used for extension:
import shallowequal from 'shallowequal';
class MyPureComponent extends Component {
shouldComponentUpdate(props, state) {
if (arguments.length < 2)
throw new Error('Do not mess super arguments up');
return !shallowequal(props, this.props) || !shallowequal(state, this.state);
}
}
class Foo extends MyPureComponent {
shouldComponentUpdate(props, state) {
if (props.selecting) {
return false;
}
return super.shouldComponentUpdate(props, state);
}
}
If you can consider writing your component in a function instead of class, try React.memo.
React.memo is a higher order component. It’s similar to React.PureComponent but for function components instead of classes.
If your function component renders the same result given the same props, you can wrap it in a call to React.memo for a performance boost in some cases by memoizing the result. This means that React will skip rendering the component, and reuse the last rendered result.
By default it will only shallowly compare complex objects in the props object. If you want control over the comparison, you can also provide a custom comparison function as the second argument.
As a second argument, you can pass a function, where you can use prevProps, nextProps, and return false, if you want it to render, or return true if you don't.
import React, { memo } from "react";
const ContentsListView = ({ selecting }) => {
return <div />;
};
const shouldComponentUpdate = (prevProps, nextProps) => {
if (nextProps.selecting) { return true; }
return JSON.stringify(prevProps) === JSON.stringify(nextProps)
}
const Component = memo(ContentsListView, shouldComponentUpdate);
I was looking for the same thing as you can see from my comment in January, I had eventually settled on using the shallow-compare library - https://www.npmjs.com/package/shallow-compare
export default class ContentsListView extends Component<Props> {
shouldComponentUpdate(nextProps, nextState) {
if (props.selecting) {
return false;
}
return shallowCompare(this, nextProps, nextState);
}
render() {
}
}
however extend from Component, not PureComponent
I'm creating a decorator or a higher-order component (HOC) for my project, which takes in a factory function, and returns another function, which wraps over the component, like this:
class X extends React.Component {
render() {
return <div>Hi</div>
}
}
export default Decorator(factory)(X);
or
#Decorator(factory)
export default class X extends React.Component {
render() {
return <div>Hi</div>
}
}
The code for my decorator is like this:
export default function Decorator(factoryFn) {
return function decorate(Component) {
return class DecoratedComponent extends Component {
static contextTypes = {
allStyles: PropTypes.object.isRequired,
}
render() {
const gStyles = factoryFn(this.context.allStyles);
return (
<Component {...this.props} gStyles={gStyles} />
);
}
};
};
}
On running this, I'm getting an error from babel-runtime/inherits.js, which says: Super expression must either be null or a function, not object.
What is the correct way to create a decorator or HOC which works like I've described above?
I m actually learning reactjs and I m actually developping a little TODO list, wrapped inside of a "parent component" called TODO.
Inside of this parent, I want to get the current state of the TODO from the concerned store, and then pass this state to child component as property.
The problem is that I dont know where to initialize my parent state values.
In fact, I m using ES6 syntax, and so, I dont have getInitialState() function. It's written in the documentation that I should use component constructor to initialize these state values.
The fact is that if I want to initialize the state inside of my constructor, the this.context (Fluxible Context) is undefined actually.
I decided to move the initialization inside of componentDidMount, but it seems to be an anti pattern, and I need another solution. Can you help me ?
Here's my actual code :
import React from 'react';
import TodoTable from './TodoTable';
import ListStore from '../stores/ListStore';
class Todo extends React.Component {
constructor(props){
super(props);
this.state = {listItem:[]};
this._onStoreChange = this._onStoreChange.bind(this);
}
static contextTypes = {
executeAction: React.PropTypes.func.isRequired,
getStore: React.PropTypes.func.isRequired
};
componentDidMount() {
this.setState(this.getStoreState()); // this is what I need to move inside of the constructor
this.context.getStore(ListStore).addChangeListener(this._onStoreChange);
}
componentWillUnmount() {
this.context.getStore(ListStore).removeChangeListener(this._onStoreChange);
}
_onStoreChange () {
this.setState(this.getStoreState());
}
getStoreState() {
return {
listItem: this.context.getStore(ListStore).getItems() // gives undefined
}
}
add(e){
this.context.executeAction(function (actionContext, payload, done) {
actionContext.dispatch('ADD_ITEM', {name:'toto', key:new Date().getTime()});
});
}
render() {
return (
<div>
<button className='waves-effect waves-light btn' onClick={this.add.bind(this)}>Add</button>
<TodoTable listItems={this.state.listItem}></TodoTable>
</div>
);
}
}
export default Todo;
As a Fluxible user you should benefit from Fluxible addons:
connectToStores.
The following example will listen to changes in FooStore and BarStore and pass foo and bar as props to the Component when it is instantiated.
class Component extends React.Component {
render() {
return (
<ul>
<li>{this.props.foo}</li>
<li>{this.props.bar}</li>
</ul>
);
}
}
Component = connectToStores(Component, [FooStore, BarStore], (context, props) => ({
foo: context.getStore(FooStore).getFoo(),
bar: context.getStore(BarStore).getBar()
}));
export default Component;
Look into fluxible example for more details. Code exсerpt:
var connectToStores = require('fluxible-addons-react/connectToStores');
var TodoStore = require('../stores/TodoStore');
...
TodoApp = connectToStores(TodoApp, [TodoStore], function (context, props) {
return {
items: context.getStore(TodoStore).getAll()
};
});
As a result you wouldn't need to call setState, all store data will be in component's props.