Pass props through a higher order component from a Route - javascript

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)

Related

How to get props in component from wrapper in React?(+TypeScript)

I have simple react-application:
export default class App extends Component {
render() {
return (
<Provider store={store}>
<Router>
<Header/>
<Switch>
<Route exact path="/" component={MainPage}/>
<AdminLayout>
<Route exact path="/admin" component={AdminPage}/>
<Route exact path="/admin/add-user" component={AddUserPage}/>
</AdminLayout>
<Route path="/sign-up" component={SignUpPage}/>
<Route path="*" component={NotFound}/>
</Switch>
</Router>
</Provider>
);
}
}
Here I needed Layout component for admin components. I wrapped them in it:
export default class AdminLayout extends React.Component<{ }, AdminLayoutState> {
state: AdminLayoutState = {
contentType: "personal"
};
setActiveMenuItem = (contentType: string) => {
this.setState({
contentType: contentType
});
};
render() {
const {
contentType
} = this.state;
return (
<Fragment>
<Segment size={"huge"}>
<Grid>
<Grid.Row>
<Grid.Column width={3}>
<MenuLeft
contentType={contentType}
onClick={(contentType) => this.setActiveMenuItem(contentType)}
/>
</Grid.Column>
<Grid.Column width={12}>
{
React.Children.map(this.props.children, function (child) {
// #ts-ignore
return React.cloneElement(child, {})
})
}
</Grid.Column>
</Grid.Row>
</Grid>
</Segment>
</Fragment>
);
}
}
I need to access layout params like contentType:
export default class AdminPage extends React.Component<AdminPageProps, AdminPageState> {
constructor(props: AdminPageProps) {
super(props);
// const dispatch = useDispatch();
}
state: AdminPageState = {
// optional second annotation for better type inference
columnNames: [
"first name",
"last name",
"mail",
"actions"
],
};
render() {
const {
columnNames,
} = this.state;
const {
contentType
} = this.props;
return (
<>
<Header>
HEADER
{contentType}
</Header>
<Divider/>
<Button primary floated={"right"}>
Add
</Button>
<UsersTable
color={"blue"}
tableType={"doctor"}
recordsLimit={50}
columnNames={columnNames}
/>
</>
);
}
}
But I get error, that propert does not exist. Then I changed child pass in Layout to:
return React.cloneElement(child, this.props)
And get Uncaught TypeError: Cannot read property 'props' of undefined
I supposed that my wrapper will get params from parent components and I will be able to access its state from child components like AdminPage, AddUserPage. Unfortunately no. How can I perform such wrapper and pass props to childs properly?
Solution #1
React.cloneElement takes 3 arguments ,
The element to clone.
The props to spread to the cloned element props.
The new children to append to the element. If omitted the original
children will remain.
So in your case you need to add the contentType prop as the second argument .
{
React.Children.map(this.props.children, function (child) {
// #ts-ignore
return React.cloneElement(child, { contentType });
});
}
what this does is { contentType } it spreads this additional props along with the props applied for that component . But in your case the above alone might not work because your children is Route component from react-router-dom . So you might need a wrapper on top of your Route Component as well .
const CustomRoute = ({component: Component, path, ...rest}) => {
return (
<Route path={path} render={(props) => <Component {...props} {...rest} />} />
);
};
Now you can replace you route inside the admin layout as
<AdminLayout>
<CustomRoute exact path="/admin" component={AdminPage}/>
<CustomRoute exact path="/admin/add-user" component={AddUserPage}/>
</AdminLayout>
solution 2 (RenderProps)
Another approach will be to use the render props pattern where you can render the children of the AdminLayout as a function .
<Grid.Column width={12}>
{this.props.children(contentType)}
</Grid.Column>
With this we can now render the routes as
<AdminLayout>
{(contentType) => {
return (
</>
<Route exact path="/admin" render={(props) => <AdminPage {...props} contentType={contentType} />} />
<Route exact path="/admin/add-user" render={(props) => <AddUserPage {...props} contentType={contentType} />} />
</>
)
}}
</AdminLayout>
IMHO the render props pattern is much more simpler as this avoids the need to create a custom Route component.
Reference
Render Props

How do you use React context inside App.js file?

I am building a multi-language app. I am using react-intl. So far, so good. I made a state of the language with context api, so I can switch it easily. However I get this error when I try to use the state in App.js: TypeError: Object is not iterable (cannot read property Symbol(Symbol.iterator)).
Here is my context file:
import React, {useState, createContext} from 'react'
export const LanguageContext = createContext();
export const LanguageProvider = (props) => {
const [language, setLanguage] = useState('')
return (
<LanguageContext.Provider value = {[language,setLanguage]}>
{props.children}
</LanguageContext.Provider>
)
}
And here is the App.js:
function App() {
const [language, setLanguage] = useContext(LanguageContext)
return (
<LanguageProvider>
//i tried using locale={language}
<I18nProvider locale={LOCALES.language}>
<CartProvider>
<TableProvider>
<div className="App">
<Router>
<Header />
<Switch>
<Route path='/Cart' component={Cart} />
<Route path='/:group/:subGroup/:item' component={Item} />
<Route path='/:group/:subGroup' component={Items} />
<Route path='/' exact component={Home} />
<Route path='/:group' component={Groups} />
</Switch>
</Router>
</div>
</TableProvider>
</CartProvider>
</I18nProvider>
</LanguageProvider>
);
}
export default App
Here is the locale file that Im using to pass to the I18nProvider :
export const LOCALES = {
ENGLISH : 'en',
FRENCH: 'fr'
}
And where I change the context value(another component, not App.js):
const [language, setLanguage] = useContext(LanguageContext)
following line is cut from jsx:
onClick={() => setLanguage('en')}
I thinks the problem might be because I am trying to access the context before the App.js return statement, where the provider wraps the children but even if this is the case, I still don't know what might fix it. Any help would be appreciated!
I thinks the problem might be because I am trying to access the context before the App.js return statement
You're right this is the problem.
Depending on where you want to use useContext you could create an extra component that is a child of LanguageProvider. Then inside this child you are able to use useContext.
To give a simplified example:
const App = () => {
const [language, setLanguage] = useContext(LanguageContext);
useEffect(() => {
setLanguage('en');
}, []);
return <p>{language}</p>;
};
export default function AppWrapper() {
return (
<LanguageProvider>
<App />
</LanguageProvider>
);
}
I had the same problem trying to apply an authentication flow with react-navigation v5. I tried to follow the documentation as it is:
Authentication flows with react-navigation v5 But when trying to mix it with Context I run into the same issue
As in the previous Answer, I solve it in the same way.
Here it's an example where it has 3 possible Screens Stacks:
I create a component where I'll be using the context
const RootStack = () => {
const { state } = useContext(AuthContext);
return (
<Stack.Navigator>
{false ? (
<Stack.Screen name="Splash" component={SplashScreen} />
) : state.token === null ? (
<Stack.Screen
name="Authentication"
component={AuthenticationStack}
options={{
title: 'Sign in',
headerShown: false,
animationTypeForReplace: false ? 'pop' : 'push',
}}
/>
) : (
<Stack.Screen name="Home" component={AppStack} />
)}
</Stack.Navigator>
);
};
And then I insert the component inside the provider:
export default ({ navigation }) => {
return (
<AuthProvider>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="RootStack"
component={RootStack}
options={{
headerShown: false,
}}
/>
</Stack.Navigator>
</NavigationContainer>
</AuthProvider>
);
};

How to display a component when certain condition true using react and javascript?

The layout component is rendered on all pages.
I want to achieve the following
in /items page
*Layout component is displayed if the user is admin
* Layout component not displayed if the user is non-admin
below is my code,
function Main() {
const isAdmin = getUser();
return(
<Switch>
<Route
exact
path="/items"
render={routeProps => (
<Layout>
{isAdmin ? <Items {...routeProps} />: <NotFound/>}
</Layout>
)}
/>
<Route
exact
path="/home"
render={routeProps => (
<Layout>
<Home {...routeProps} />
</Layout>
)}
/>
</Switch>
);
}
const Layout: React.FC = ({ children }) => (
<>
<TopBar />
{children}
<BottomBar />
</>
);
As you see from the above code, the Layout component is displayed in all pages and is used as a wrapper for other routes too like for /home
Now I don't want the Layout component to be displayed only in /items page if a user is not admin
What I have tried?
const Layout: React.FC = ({ children }) => {
const history = useHistory();
const isItemsPath = history.location.pathname.includes('/items');
const isAdmin = getUser();
return (
<>
{!isItemsPath && <TopBar />
{children}
{!isItemsPath && <BottomBar />
</>
);
}
But this will not display TopBar and BottomBar if the items page even if the user is admin. how can I modify the condition
such that TopBar and BottomBar are displayed in all pages except items page if not admin.
could someone help me with this? thanks.
};
In your layout component you can use conditional rendering. We can check if the page is isItemsPath first, if it is items path and user is not admin then we do not show the Topbar and BottomBar, for all other pages we show them
const Layout: React.FC = ({ children }) => {
const history = useHistory();
const isItemsPath = history.location.pathname.includes('/items');
const isAdmin = getUser();
return !(isItemsPath && !isAdmin) ?
<>
{children}
</> : (
<>
<TopBar />
{children}
<BottomBar />
</>
);
}
What about you change the condition in Route?
<Route
exact
path="/items"
render={routeProps => (
{isAdmin ? <Layout>
<Items {...routeProps} />
</Layout>
: <NotFound/>}
)}
/>
If I understand correctly, you might be looking for something along the lines of this:
const Layout: React.FC = ({ children }) => {
const history = useHistory();
const isItemsPath = history.location.pathname.includes('/items');
const isAdmin = getUser();
return (
<>
{(!isItemsPath || isAdmin) && <TopBar />
{children}
{(!isItemsPath || isAdmin) && <BottomBar />
</>
);
Then you should be able to remove your isAdmin condition in your Main component.
You have some choice to doing this, but I think the best way is using HOC if you have to repeat checking the user is admin or not, pass your component to HOC and in HOC component check if a user is an admin or not. You can use this HOC component for all of your components. In HOC component use conditional rendering. Something like this :
function checkAdmin(WrappedComponent, selectData) {
const isAdmin = getUser();
render() {
return ({isAdmin} ? <WrappedComponent /> : <div></div>)
}
}

How to access state from one child component to another child component using react and typescript?

I want to set state in one child component and access it in another child component using react and typescript.
What I am trying to do?
In component child1 I set isDialogOpen state and I need to access it in child2 component.
Below is how the structure is,
function Main() {
return (
<Wrapper>
<React.Suspense fallback={null}>
<Switch>
<Route
exact
path="/page1"
render={routeProps => (
<Layout>
<React.Suspense fallback={<PlaceHolder></>}>
<child1 {...routeProps} />
</React.Suspense>
</Layout>
)}
/>
<Route
exact
path="/page2"
render={routeProps => (
<Layout>
<Child2 {...routeProps} />
</Layout>
)}
/>
//many other components rendered apart from child1 and child2
</Switch>
</React>
</Wrapper>
)
}
function Child1() {
return (
<UploadButton/>
);
}
type Props = RouteComponentProps<{ itemId: string; productId: string }>;
function UploadButton({ match }: Props) { //here i set the state isDialogOpen
const [isDialogOpen, setDialogOpen] = React.useState(false);
const handle_click = () => {
setDialogOpen(!isDialogOpen);
};
return (
<>
<Button onClick={handle_click}/>
{isDialogOpen && (
<UploadForm/>
)}
</>
);
}
function Child2() {
return (
<UserButton/>
);
}
function UserButton() {
return (
<Icon/>
);
}
Here in the Main component, I am rendering child1 and child2 components.
UploadButton component is where isDialogOpen state is set.
Now when the isDialogOpen is true I want the Icon in UserButton component to be rendered.
Now the question is to access isDialogOpen state in the UserButton component I have to define a function to set the state in the main component and pass it to child1 -> UploadButton, and pass the state to the child2 component -> UserButton
As I want to pass the state so many levels the other solution could be to use context API.
So I have tried like below,
interface DialogCtxState {
isDialogOpen: boolean;
setIsDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
}
const initialDialogState: DialogCtxState = {
isDialogOpen: false,
setIsDialogOpen: () => {},
};
export const LoadingContext = React.createContext<DialogCtxState>(
initialDialogState
);
export const DialogContextProvider: React.FC = ({ children }) => {
const [isDialogOpen, setIsDialogOpen] = React.useState<boolean>(false);
return (
<DialogContext.Provider
value={{
isDialogOpen,
setIsDialogOpen,
}}
>
{children}
</DialogContext.Provider>
);
}
//wrap contextprovider for main
function Main() {
return (
<DialogContextProvider>
<Wrapper>
<React.Suspense fallback={null}>
<Switch>
<Route
exact
path="/page1"
render={routeProps => (
<Layout>
<React.Suspense fallback={<PlaceHolder></>}>
<child1 {...routeProps} />
</React.Suspense>
</Layout>
)}
/>
<Route
exact
path="/page2"
render={routeProps => (
<Layout>
<Child2 {...routeProps} />
</Layout>
)}
/>
//many other components rendered apart from child1 and child2
</Switch>
</React>
</Wrapper>
</DialogContextProvider>
)
}
//set the isDialogOpen in UploadButton as below
function UploadButton({ match }: Props) {
const { isDialogOpen, setIsDialogOpen } = React.useContext(LoadingContext);
const handle_click = () => {
setIsDialogOpen(!isDialogOpen);
};
return (
<>
<Button onClick={handle_click}/>
{isDialogOpen && (
<UploadForm/>
)}
</>
);
}
function UserButton() {
const { isDialogOpen } = React.useContext(LoadingContext);
return (
{isDialogOpen && <Icon/>}
);
}
this works, but I don't want to wrap the DialogContextProvider around all child components of main. instead, I want the context to be used only in UploadButton, UploadForm and UserButton Components only.
How can I do this? Also, I have many other contextproviders wrapped in main. it kind of looks clumsy.
How can I do this other way? Or could someone help me with some better solution thanks.

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>
}
}

Categories