How to forward ref in a HOC in React? - javascript

I am using Context API to use themes in my React Native project. To consume the Theme Context, I made a Higher Order Component and passed them into the component as props.
This worked fine for the most of the app. But when I started using refs, it started crashing, because as per the documentation, the refs will not be forwarded automatically, and we need to use the React.forwardRef API. But something when wrong with my implementation and I am not able to resolve it.
Here's the code I have been working on:
// Higher order component, to wrap my component with theme (withTheme.js)
export const withTheme = (Component, areRefsUsed = false) => {
// Logic to check if refs are being used
if (areRefsUsed) {
const ThemedComponent = (props, ref) => {
return (
<ThemeContext.Consumer>
{theme => (<Component {...props} theme={theme} ref={ref} />)}
</ThemeContext.Consumer>
)
};
return React.forwardRef(ThemedComponent);
}
return (props) => {
return (
<ThemeContext.Consumer>
{theme => <Component {...props} theme={theme} />}
</ThemeContext.Consumer>
)
}
};
// My component where I am using refs (ExampleComponent.js)
class ExampleComponent extends Component {
constructor(props) {
super(props);
this.refForSomeComponent = React.createRef();
}
render() {
const { theme } = this.props;
return (
<Fragment>
<Text style={{color: theme.primaryColor}}>It is working</Text>
<TextInput ref={this.refForSomeComponent} value={'Test'} />
</Fragment>
)
}
}
export default withTheme(ExampleComponent, true);
When I try to run the app, this error is being thrown:
The component for route 'ExampleComponent' must be a React component.

Related

How to render React Route component in an entirely new, blank page

I'm trying to render a print page using React Router. So I have two components:
export default class PurchaseOrder extends React.Component{
....
render(){
const {orderDate, client} = this.state.order;
//omitted for brevity
return(
<BrowserRoute>
<Button
component={Link}
to="/order/print"
target="_blank"
>
Print
</Button>
<Route
path="/order/print"
render={props => (
<OrderPrint
{...props}
orderDate={orderDate}
client={client}
/>
)}
/>
</BrowserRoute>
}
}
And the OrderPrint:
export default function OrderPrint(props) {
return (
<div>props.orderDate</div>
<div>props.client.name</div>
);
}
As you can see, I'm trying to present the printable version of the purchase order with a click of a button. The OrderPrint component gets rendered, but it's rendered right below the button. I could put the Route inside my root component, which is App, that way making sure that I get only the contents of the OrderPrint component rendered like this:
class App extends Component {
render() {
return (
<div className="App">
<Router>
<Route exact path="/" component={PurchaseOrder} />
<Route exact path="/order/print" component={OrderPrint} />
</Router>
</div>
);
}
}
But in that case, I won't be able to pass the necessary props to it. So in this particular case, how to replace entire page content with the contents of OrderPrint component and still be able to pass the necessary input to it?
Update
As #Akalanka Weerasooriya mentioned in comments, I could have the entire state kept in the App component. But one thing stopped me from doing this: This means I'll practically always have to use the render prop of the Route component, instead of the component prop. Ok, that's not a problem, but if it's the way to go, then why does React Router documentation almost always use the
<Route path="/about" component={About} />
pattern as the standard way of using it? So to recap it, if I go the Single Source of Truth way and store all my state in one place, then doesn't it mean that I will always use
<Route path="/about" render={props=>(<div>props.someProp</div>)} />
I don't say there's a problem with it, it's just mentioning it in the documentation only after component={SomeComponent} pattern confuses me.
Not sure why you need a different route for a print page, but anyway if you want it on a new empty page, you can take advantage of the ReactDOM.createPortal feature.
You can create a new page and or even a new window using window.open while keeping the flow of react data in sync.
Here is a running example of a portal on a new window with live state updates from the component that triggered this window using a portal:
running example, i'm sharing an external snippet and not using stack-snippets here because window.open returns null in the contexts of stack-snippets
Source code:
class WindowPortal extends React.PureComponent {
containerEl = document.createElement("div");
externalWindow = null;
componentDidMount() {
const { width = 450, height = 250, left = 150, top = 150 } = this.props;
const windowFetures = `width=${width},height=${height},left=${left},top=${top}`;
this.externalWindow = window.open("", "", windowFetures);
this.externalWindow.document.body.appendChild(this.containerEl);
}
componentWillUnmount() {
this.externalWindow.close();
}
render() {
return ReactDOM.createPortal(this.props.children, this.containerEl);
}
}
class App extends React.PureComponent {
state = {
counter: 0,
showWindowPortal: false
};
componentDidMount() {
window.setInterval(() => {
this.setState(state => ({
counter: state.counter + 1
}));
}, 1000);
}
toggleWindowPortal = () => {
this.setState(state => ({
...state,
showWindowPortal: !state.showWindowPortal
}));
};
closeWindowPortal = () => {
this.setState({ showWindowPortal: false });
};
render() {
return (
<div>
<h1>Counter: {this.state.counter}</h1>
<button onClick={this.toggleWindowPortal}>
{this.state.showWindowPortal ? "Close the" : "Open a"} Portal
</button>
{this.state.showWindowPortal && (
<WindowPortal closeWindowPortal={this.closeWindowPortal}>
<h2>We are in a portal on a new window</h2>
<h3>{`This is the current state: ${this.state.counter}`}</h3>
<p>different window but sharing the state!!</p>
<button onClick={() => this.closeWindowPortal()}>Close me!</button>
</WindowPortal>
)}
</div>
);
}
}
here you have a PrivateRoute which is a custom route which holds a header and header is rendered in PrivateRoute routes only so when you try to navigate to new route like path="/order/print" then you won't get header which has button in it.
function Header(props) {
return (
<div>
<Button
component={Link}
to="/order/print"
target="_blank">
Print</Button>
{props.children}
</div>
)
}
const PrivateRoute = ({ component: Component, layout: Layout, ...rest }) => {
return <Route {...rest} render={props => {
return <Layout>
<Component {...props} />
</Layout>
}} />
}
export default class PurchaseOrder extends React.Component{
render(){
const {orderDate, client} = this.state.order;
//omitted for brevity
return(
<BrowserRouter>
<div>
<PrivateRoute exact path="/" layout={Header} component={Landing} />
<Route
path="/order/print"
render={props => (
<OrderPrint
{...props}
orderDate={orderDate}
client={client}
/>
)}
/>
</div>
</BrowserRouter>
}
}

react - How to open show more on a function component?

I have a functional component
const text = ({data}) => {
return (
<p onClick={()=> render more?}>info</p>
)}
const more = ({data}) => {
return (<p>..........</p>)
}
Is it possible to render more component on the onClick event?
Sure, you'll need a state variable. Use the state to determine whether to render more or not, and then set the state when the click happens. If you have react 16.8 or later, you can do this in a functional component with hooks:
import { useState } from 'react';
const MyComponent = ({data}) => {
const [showMore, setShowMore] = useState(false);
return (
<div>
<p onClick={() => setShowMore(true)}>info</p>
{showMore && <More data={data} />}
</div>
)}
}
Prior to 16.8, you'll need to use a class component.
class MyComponent extends React.Component {
state = {
showMore: false,
}
render() {
return (
<div>
<p onClick={() => this.setState({ showMore: true})}>info</p>
{this.state.showMore && <More data={this.props.data} />}
</div>
)}
}
}

Passing props to a react component wrapped in withRouter() function

I am using React-Router v4 to navigate in my React app. The following is a component wrapped in the withRouter() function to make it able to change route on click:
const LogoName = withRouter(({history, props}) => (
<h1
{...props}
onClick={() => {history.push('/')}}>
BandMate
</h1>
));
As you can see I pass the props to the component, which I need in order to change the class of the component. The problem here is that props is undefined in the <LogoName> component. I need to be able to change the class of this component when I click on another component, like this:
<LogoName className={this.state.searchOpen ? "hidden" : ""} />
<div id="search-container">
<SearchIcon
onClick={this.handleClick}
className={this.state.searchOpen ? "active" : ""} />
<SearchBar className={this.state.searchOpen ? "active" : ""}/>
</div>
Here is how I handle the click. Basically just setting the state.
constructor(){
super();
this.state = {
searchOpen: false
}
}
handleClick = () => {
this.setState( {searchOpen: !this.state.searchOpen} );
}
Is there a way for me to pass props to a component that is wrapped inside the withRouter() function or is there a similar way to create a component which has the ability to navigate with React-Router and still receive props?
Thanks in advance.
The problem is that while destructuring, you want to destructure props but you are not passing any prop named props to LogoName component
You can change your argument to
const LogoName = withRouter((props) => (
<h1
{...props}
onClick={() => {props.history.push('/')}}>
BandMate
</h1>
));
However you can still destructure the props like #Danny also suggested by using the spread operator syntax like
const LogoName = withRouter(({history, ...props}) => (
<h1
{...props}
onClick={() => {history.push('/')}}>
BandMate
</h1>
));
You're close, just spread the props in your function signature as well:
const LogoName = withRouter(({ history, ...props }) => (
<h1
{...props}
onClick={() => {history.push('/')}}>
BandMate
</h1>
));
This worked for me:
import {withRouter} from 'react-router-dom';
class Login extends React.Component
{
handleClick=()=>{
this.props.history.push('/page');
}
render()
{
return(
<div>
.......
<button onClick={this.handleClick()}>Redirect</button>
</div>);
}
}
export default withRouter(({history})=>{
const classes = useStyles();
return (
<Login history={history} classes={classes} />
)
});

Pass props through a higher order component from a Route

I have a problem with my Higher Order Component. I am trying to pass props from a <Layout /> component down a route (React Router v4). The components specified in the routes are wrapped by a HOC, but the props that I pass never reaches the component.
Also, I can't use the HOC without using export default () => MyHOC(MyComponent). I can't figure out why, but that might have something to do with it?
Layout.js
const Layout = ({ location, initialData, routeData, authenticateUser }) => (
<Wrapper>
<Container>
<Switch>
// how do I get these props passed through the HOC? render instead of component made no difference.
<Route exact path="/pages/page-one" component={() => <PageOne routeData={routeData} title="PageOne" />} />
<Route exact path="/pages/page-two" component={() => <PageTwo routeData={routeData} title="PageTwo" />} />
<Route component={NotFound} />
</Switch>
</Container>
</Wrapper>
)
export default Layout
Page.js
// I've tried swapping to (WrappedComponent) => (props) without success
const Page = (props) => (WrappedComponent) => {
const renderHeader = props.header
? <Header title={props.headerTitle} />
: false
return (
<Wrapper>
{renderHeader}
<Container withHeader={props.header}>
<WrappedComponent />
</Container>
</Wrapper>
)
}
export default Page
PageOne.js
class PageOne extends React.Component {
render() {
return (
<Content>
<Title>{this.props.title}</Title> // <----- not working!
{JSON.stringify(this.props.routeData, null, 4)} // <---- not working!
</Content>
)
}
}
export default () => Page({ header: true, headerTitle: 'header title' })(PageOne)
// does not work without () => Page
// when using just export default Page I get the "Invariant Violation: Element type is invalid:
// expected a string (for built-in components) or a class/function (for composite components)
// but got: object. Check the render method of Route." error.
You need one more arrow to make your Page to be a HOC. It takes params, wrapped component and has to return a component. Yours were rendering after getting WrappedComponent
const Page = (props) => (WrappedComponent) => (moreProps) => {
const renderHeader = props.header
? <Header title={props.headerTitle} />
: false
return (
<Wrapper>
{renderHeader}
<Container withHeader={props.header}>
<WrappedComponent {...moreProps} />
</Container>
</Wrapper>
)
}
Now you can use it like this
export default Page({ header: true, headerTitle: 'header title' })(PageOne)

How to make component re-render when value from outside changes?

So, I have simple code (class) like this:
export default class LoginAction {
isLoggedIn = () => {
return true
}
}
And I used it in my other classes like this:
export default class Main extends Component {
render = () => {
const loginAction = new LoginAction()
if (loginAction.isLoggedIn()) {
return (
<View style={{ flex: 1 }}>
<Header headerText={'Post List'} />
<PostList />
</View>
)
}
....... (split)
}
}
The question is, when I change the return value on the isLoggedIn function, why Main component not re-rendered?
It's React Native, and I use Hot Reloading.
A component re-renders only in 2 situations:
if its state has changed
if the received props have changed
In your Main component, none of these situations happen.
To fix it, you could pass isLoggedIn to your component:
// index.js
const loginAction = new LoginAction()
let isLoggedIn = loginAction.isLoggedIn()
const setLoggedUser = user => {
loginAction.setLoggedUser(user)
isLoggedIn = true
}
ReactDOM.render(
<div>
{!isLoggedIn && <Login setLoggedUser={setLoggedUser} />}
<Main isLoggedIn={isLoggedIn} />
</div>,
document.getElementById('root')
)
And use this prop in your component's render:
export default class Main extends Component {
render = () => {
if (this.props.isLoggedIn) {
return (
<View style={{ flex: 1 }}>
<Header headerText={'Post List'} />
<PostList />
</View>
)
}
...
}
}
In doing so, your component will re-render when isLoggedIn changes.

Categories