The Portal component is being called twice.
Why is it happening?
How can I prevent that?
index.js
const App = () => {
const theme = lightTheme;
return (
<Provider store={store}>
<StyleSheetManager>
<ThemeProvider theme={theme}>
<BrowserRouter>
<Portal />
</BrowserRouter>
<GlobalStyle />
</ThemeProvider>
</StyleSheetManager>
</Provider>
);
};
ReactDOM.render(<App />, document.getElementById("app"));
Portal.jsx
class Portal extends React.Component {
isAuthenticated = () => {
if (this.props.tokenized) {
return (
<React.Fragment>
<Sidebar />
<MainContainer />
<Switch>
<Route exact path="/"></Route>
</Switch>
</React.Fragment>
);
} else {
return <Authorize />;
}
};
render() {
this.props.dispatch(checkToken());
return (
<React.Fragment>
<Alerts />
{this.isAuthenticated()}
</React.Fragment>
);
}
}
export default connect(
({ authenticator }) => ({
tokenized: authenticator.tokenized,
}),
null
)(Portal);
This happens as you're dispatching an action on every render and the redux state is probably getting updated after the first dispatch call to it.
When your Portal renders for the first time, yu dispatch checkToken() which checks for token probably and updates authenticator.tokenized. Since your component is connected to the store to map state to props, your prop tokenized carries a new value therefore causing a re-render. BUT, rendering twice of this particular component is not erronuous as the token can be only avilable after some time(after the first render of the component).
You should memoize the component so that you can perform checks to incoming props with previous props to avoid unnecessary re-renders.
Also, your checkToken call should happen only once when the component loads (in componentDidMount) and not everytime in each render method execution.
Related
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>
}
}
guys, first of all, it may look like a duplicate, but I swear, I searched all over the place and all the possible solutions didn't worked for me.
The problem is the classic "When I click on a element the URL changes but the component doesn't update"
In fact, it updates, but only on the first click.
Here is my relevant code:
index.js
...
render(
<Provider store={store}>
<ConnectedRouter history={history}>
<div>
<App />
</div>
</ConnectedRouter>
</Provider>,
target
);
App component
...
const App = () => (
<BrowserRouter>
<div>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/items/:id" component={Product} />
<Route path="/items" component={Search} />
</Switch>
</div>
</BrowserRouter>
);
export default App;
Search component
...
const Search = props => (
<div>
<header>
<SearchBar />
</header>
<main>
<SearchResult />
</main>
</div>
);
export default Search;
SearchResult component
const renderItem = result => (
<div key={result.id} className="result-item">
<Link to={`/items/${result.id}`}>
...
</Link>
</div>
);
class SearchResult extends Component {
componentWillMount() {
if (!this.props.resultIsLoading && !this.props.results.items) {
const { search: term } = queryString.parse(this.props.location.search);
this.props.search(term);
}
}
render() {
if (!this.props.results.items) return <div>...</div>;
return (
<div className="container">
<div className="product-list">
{this.props.results.items.map(renderItem)}
</div>
</div>
);
}
}
const mapStateToProps = ({ results, resultIsLoading }) =>
({ results, resultIsLoading });
const mapDispatchToProps = dispatch =>
bindActionCreators({ search }, dispatch);
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(SearchResult));
When I click the , the state doesn't change, the reducer is not called and the ProductDetails component doesn't update.
I've tried a lot of possible solutions. { pure: false } on connect, pass location as a prop, remove main BrowserRouter, remove withRouter, none seems to work.
Can, please, someone give me a light on this?
I am not sure if it's anti-pattern, I've created a HOC at routes level and fetching user details in it like this:
class PrivateRoute extends Component {
render() {
const { component : RouteComponent, checkAuth, ...rest } = this.props
const route = (
<Route
{...rest}
render={(props) => {
return checkAuth() === true
? <RouteComponent {...props} />
: <Redirect to={{pathname : '/login'}} />
}}
/>
)
return route
}
componentWillMount() {
const { store } = this.props
const { user } = store.getState()
const accessToken = getCookie('access_token')
if (!user.userInfo && accessToken) {
store.dispatch(getUserDetails(accessToken)) // Setting user details in redux store.
}
}
}
const routes = (checkAuth, store) => (
<div className='app grid'>
<ConnectedNavbarContainer />
<div className='col'>
<Switch>
<Redirect exact={true} from='/' to='/dashboard' />
<Route path='/login' component={ConnectedLoginContainer} />
<PrivateRoute
store={store}
checkAuth={checkAuth}
path='/:id/dashboard'
component={ConnectedDashboardContainer}
/>
<PrivateRoute
store={store}
checkAuth={checkAuth}
path='/select-store'
component={ConnectedSelectStoreContainer}
/>
</Switch>
</div>
</div>
)
Observe in componentWillMount function of PrivateRoute, I am fetching user details and saving it in redux store. Now the problem is getUserDetails is async call and the child component of PrivateRoute will make network calls to render that page and those network calls need userID which will be present in the getUserDetails call's response.
Now I want the child component to wait until the getUserDetails call is resolved. I am not sure how to do it.
Create an action that will set a state in the redux store to reflect that user details are being retrieved. This will enable to child component to know that user details are not ready yet, and display an appropriate message (e.g. 'loading...' or 'initializing...').
When user details are retrieved, the above state should be updated appropriately. If the retrieval of user details has failed, fire another action that will set the state to error (and clearing the 'loading in process' state), to enable the child to display an error message.
I have a component tree like this:
-App
--UserList
---UserItem
---UserItem
---UserItem
--User
I'm not being able to pass user data from UserItem to User. This is what I have:
App.js
export default class App extends Component {
state = { users: [] }
componentDidMount() {// fetch and setState}
render() {
return (
<BrowserRouter>
<Route
exact
path="/"
render={() => <UserList users={this.state.users} />}
/>
</BrowserRouter>
)
}
}
UserList.js
export default function({ users }) {
return (
<div>
{users.map(user => (
<UserItem user={user} key={`${user.id}`} />
))}
</div>
)
}
This is where the problem is: I want to pass the data from the parent component to the child User component, instead of having to fetch the user data from the API again.
UserItem.js
export default function({ user }) {
return (
<div>
<Link to="/user">{user.name}</Link>
<Route path={`/user/${user.name}`} render={() => <User user={user} />} />
</div>
)
}
I'm not sure what you're trying to implement here. Your app renders the UserList when then route is /. The UserList renders a UserItem component for each user in the array. Each UserItem simply renders a route specific to every user, which will render the User component if that route is triggered.
But if I'm not mistaken, the UserList will not be rendered if the route is anything but /, so if someone accesses user/..., the inner routes won't actually exist.
Essentially, this app will not render anything.
If you remove the exact keyword from the route in App, I think you'll get the result you are looking for. In this case, opening /user/<USER_NAME> will render the User element for that user.
Your question is regarding passing props into a component through a route, and the mechanism you've used is correct.
<Route path={...} render={() => <User user={user} />} />
This is actually right. See the code linked below. On changing the route to /user/User1, you'll see the name of "User1" rendered in the app.
See the working code here: https://codesandbox.io/s/18w3393767
You should use this.props.users in the UserItem component
i'm not sure but could you pass props like below, here i pass props to render and then to User Component
<Route path={`/user/${user.name}`} render={(props) => <User user={user} {...props} />} />
export default function({ users }) {
return (
<div>
{ this.props.users.map(user => (
//mistake here this.props.users.map not users.map
<UserItem user={user} key={`${user.id}`} />
))}
</div>
)
}
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)